Fluid Select mit Objekten in Hierarchie darstellen

Folgendes Datenmodell: Objekte vom Typ ‚Items‘ und Kategorien. Eine Kategorie hat eigentlich nur einen Titel, kann aber über das Feld ‚parent‘ einer anderen Kategorie als Kind zugewiesen werden, so dass eine Art Baum entsteht. So ist z.B. in Kategorie A.1 ist Kategorie A als Parent gesetzt.

- A
-- A.1
-- A.2
--- A.2.I
--- A.2.II
-- A.3
- B

Nun will ich in einem Select-Feld die Kategorien ausgeben, dabei würde ich gerne den vollen Pfad zur Kategorie im Select ausgeben und nicht nur den Titel der Kategorie. So würde dann die Auswahl im Select-Feld so aussehen:

A
A > A.1
A > A.2
A > A.2.I
[...]

Und das ist meine (wie ich finde elegante) Lösung: das Select-Feld in Fluid wird dann so definitert:

<f:form.select property="category" options="{categories}" prependOptionLabel="Bitte wählen" prependOptionValue="" optionLabelField="titlePath"/>

Die Eigenschaft titlePath exisitert in der Kategorie natürlich nicht, aber Fluid bedient sich da des Getters, also muss nur der entsprechende Getter definiert werden. In dem Getter ruft man dann die gleiche Funktion vom Parent auf. That’s it!

/**
 * @return string
 */
public function getTitlePath() {
    if($this->parent == null) {
        return $this->title;
    }
    return $this->parent->getTitlePath().' - '.$this->getTitle();
}

Fluid Formular für neues Objekt mit Select und leerer Option

Ich implementiere gerade eine Extension mit (vereinfacht) folgendem Aufbau: Objekte vom Typ ‚Items‘ können jeweils einer Kategorie zugeordnet werden. Die Kategorien sind wiederum in der Datenbank als Datensätze hinterlegt. So etwas kommt häufig vor, das beliebteste Beispiel ist wahrscheinlich ein Blog-Eintrag und die dazugehörige Kategorie.

Erstellt man diese Extension z.B. über den Extension Builder und lässt sich eine new/create-Action anlegen, kann man im Frontend nun auch Objekte vom Typ ‚Item‘ anlegen. Nun hatte ich folgende Hürde zu meistern: das Feld Kategorie ist ein Pflichtfeld, trotzdem soll in der Kategorieauswahl im Frontend eine zusätzliche leere Option angezeigt werden. Da höre ich schon den Schrei der Empörung: „Warum brauchst du eine leere Option? Das Feld ist doch ein Pflichtfeld!“. Weil dann der Benutzer gezwungen ist, sich für eine Option bewusst zu entscheiden und nicht einfach die erste Option angenommen wird.

Eine leere Option im Formular dranzuhängen ist einfach:

<f:form.select property="category" options="{categories}" prependOptionLabel="Bitte wählen" prependOptionValue="" optionLabelField="title" />

Im Model das Feld als Pflichtfeld deklarieren:

/**
 * @var \MY\MyExtension\Domain\Model\Category
 * @validate NotEmpty
 */
protected $category = null;

Wenn man nun das Formular im Frontend testet, passiert folgendes: beim Absenden des Formulars ohne etwas auszuwählen gibt es eine Exception: Exception while property mapping at property path "": PHP Catchable Fatal Error: Argument 1 passed to MY\MyExtension\Domain\Model\Item::setCategory() must be an instance of MY\MyExtension\Domain\Model\Category, null given. Das erwartete Verhalten wäre aber, dass der Validator greift und eine Fehlermeldung ausgibt.

Die Lösung ist das Setzen eines Default-Wertes im Setter:

/**
 * Sets the category
 *
 * @param \MY\MyExtension\Domain\Model\Category $category
 * @return void
 */
public function setCategory(\MY\MyExtension\Domain\Model\Category $category = null)
{
    $this->category = $category;
}

TYPO3 7.6. Indexed Search – Ausgabe optimieren

Lange habe ich mich nicht an die zweite Implementierung der Indexed Search rangetraut. Der Hinweis „experimental“ lädt auch nicht gerade ein, das Plugin einzusetzen. Mittlerweile ist das „experimental“ dem „Extbase/Fluid based“ gewichen und suggeriert, dass es nun stabil sei. Die Vorteile von dem Plugin gegenüber dem Original liegen auf der Hand – endlich saubere Templates! Wenn ich nur daran denke, welche Konfiguration und CSS-Gefrickel teilweise vonnöten war, um das gewünschte Layout des Designers mit dem alten Plugin umzusetzen. Nun soll es damit ja vorbei sein. Wenn es doch so einfach wäre.

Neu in TYPO3 ist, dass man nicht alle Templates überschreiben muss, sondern einen Template-Order angeben kann, in dem man einige der Dateien ablegt und der Rest wird als Fallback aus der Extension geholt. Das funktioniert in Indexed Search nicht so gut, dazu hatte ich ja schonmal was geschrieben: Templates Indexed Search überschreiben. Die Konfiguration muss wie folgt angepasst werden, damit man in den Konstanten den Pfad zu den überschreibenden Dateien angeben kann.

plugin.tx_indexedsearch {
	view {
		templateRootPath >
		templateRootPaths {
			0 = EXT:indexed_search/Resources/Private/Templates/
			1 = {$plugin.tx_indexedsearch.view.templateRootPath}
		}
 
		partialRootPath >
		partialRootPaths {
			0 = EXT:indexed_search/Resources/Private/Partials/
			1 = {$plugin.tx_indexedsearch.view.partialRootPath}
		}
	}
}

So weit so gut. Nächstes Problem – der Suchbegriff wird in der Anzeige der Suchtreffer nicht hervorgehoben. Natürlich könnte man es dann auch mit einem ViewHelper lösen, ABER der Mechanismus zum Hervorheben des Suchbegriffs sorgt auch dafür, dass der relevante Abschnitt der Zusammenfassung in der Trefferliste angezeigt wird. Soll heißen: Wenn der Suchbegriff unten auf der Seite gefunden wird, ich aber die ersten 200 Zeichen des Textes bekomme, dann bringt mir der ViewHelper auch nichts.

Nachdem ich Google mehrfach bemüht habe, um nur ansatzweise jemanden mit dem gleichen Problem zu finden, wurde ich doch fündig auf Stackoverflow. Im Indexed Search Controller ist in Zeile 452 folgendes zu finden:

$resultData['description'] = $this->makeDescription(
    $row,
    (bool)!($this->searchData['extResume'] && !$headerOnly),
    $this->settings['results.']['summaryCropAfter']
);

Damit der Suchbegriff hervorgehoben wird, muss der zweite Parameter false sein. Ich habe über diese Stelle meditiert, um zu verstehen, was diese Abfrage macht und in welchem Fall sie mal false ist. Aller Einstellungen zum Trotz ist es nie der Fall. Interessanterweise wird der Suchbegriff hervorgehoben, wenn ich bei gleichen Konfiguration das Classic-Plugin für die Ausgabe nutze. Verrückt, oder?

Lösung: XCLASS! Extbase-Style geht das so: in ext_localconf.php die Klasse angeben, die den Controller überschreibt.

$GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects']['TYPO3\\CMS\\IndexedSearch\\Controller\\SearchController'] = array(
    'className' => 'NP\\MyExtension\\Controller\\SearchController'
);

Dann im eigenen Controller die Funktion compileSingleResultRow übernehmen und überschreiben:

$resultData['description'] = $this->makeDescription(
    $row,
    false,
    $this->settings['results.']['summaryCropAfter']
);

Yeah, der Suchbegriff wird hervorgehoben. Damit das auch gut aussieht, muss man im Template die Description als RTE parsen:

<f:format.html>{row.description}</f:format.html>

Nächstes Problem: Falls der Suchbegriff in einem Link vorkommt, dann ist der Link schrott. Beispiel: Auf der gesuchten Seite exisitert der Link http://www.test.de, der Suchbegriff ist test. Die Funktion zum Hervorheben wrappt den Suchbegriff in einen Tag (span mit Klasse dran, egal), Ergebnis: http://www.<span class=“…“>test</span>.de. Bei der Ausgabe geht der RTE Parser dahin, aha, ein Link und macht folgendes: <a href=“http://www.“>http://www.</a><span class=“…“>test</span>.de. Es entsteht ein völlig verkrüppelter Link. Bis ich rausgefunden habe, dass es der RTE macht, waren 3 Tassen Kaffee verbraucht. Lösung: <f:format.raw>{row.description}</f:format.raw> statt dem RTE Parser.

Wenn jemand weiß, wie das alles weniger kompliziert gemacht werden kann, ich bin gerne für Vorschläge offen.

TYPO3: Inhaltselemente nummerieren

Für die Umsetzung von diesem Template müssen die Inhaltselemente mit einer Klasse und einer Id versehen werden, die die Position des Inhaltselements in der Spalte enthält – die Elemente werden durchgezählt.

Gewünschtes Ergebnis:

<!-- OPEN - LEFT PART -->
<div class="ms-left animated-middle">
	<div class="ms-section" id="left1">
		INHALTSELEMENT
	</div>
	<div class="ms-section" id="left2">
		INHALTSELEMENT
	</div>
	<div class="ms-section" id="left3">
		INHALTSELEMENT
	</div>
	<div class="ms-section" id="left4">
		INHALTSELEMENT
	</div>
</div>
<!-- CLOSE - LEFT PART -->

Und das erreicht man durch folgendes TypoScript:

lib.contentLeft < styles.content.get
lib.contentLeft {
    renderObj = COA
    renderObj {
        10 = LOAD_REGISTER
        10 {
            Counter.cObject = TEXT
            Counter.cObject.data = register:Counter
            Counter.cObject.wrap = |+1
            Counter.prioriCalc = intval
        }
 
        20 = TEXT
        20 {
            insertData = 1
            value = <div class="ms-section" id="left{register:Counter}">
        }
 
        40 < tt_content
        50 = TEXT
        50.value = </div>
    }
}

Ich liebe TYPO3!

Newsletter-Links der News-Detailseiten führen auf 404-Seite

Problem: Im Newsletter (Beispiel Cleverreach) eingesetzte Links auf News-Detailseiten führen auf eine 404-Seite, weil an die Links zusätzliche Parameter drangehängt werden in der Form ?utm_source=cleverreach&utm_medium=email&utm_term=XXX&utm_content=XXX. Dank Google und TYPO3 Forum schnell eine Lösung gefunden.

Lösung: Die Liste der Parameter in der Konfiguration ausschließen. Dazu übers Install-Tool die Einstellung cHashExcludeParameters um folgende Angabe erweitern: ,utm_source,utm_medium,utm_content,utm_term,utm_campaign

Workaround für Übersetztungen der Datensätze mit FAL Media

Der TYPO3 Bug #57272: Extbase doesn’t handle FAL translations correctly existiert bereits seit März 2014 und immer noch gibt es keine Lösung dafür. Das Problem ist folgendes: Datensätze mit einer FAL Relation können nicht vernünftig übersetzt werden.

Ist unter config der sys_language_mode auf content_fallback eingestellt, dann werden bei einer anderen Sprache als Standard trotz abweichender FAL Referenzen nur die Referenzen der Standardsprache angezeigt. Ist der sys_language_mode auf strict eingestellt, dann werden keine FAL Medien angezeigt. Das betrifft z.B. die Extension news und in meinem Fall eine selbst geschriebene Extension mit FAL Dateireferenzen.

Ich habe im Internet gesucht und keine Lösung gefunden. Es gab Lösungsansätze: manuelles holen der Referenzen, ViewHelper, um die lokalisierten FAL Medien zu holen. Ich finde das alles nicht optimal. Meine Lösung daher: den Redakteur die Sprache der Referenz (sys_file_reference) setzen lassen:

image-fal-translation

Die Vorteile, die ich dabei sehe: es ist alles ohne Hooks und somit sauber realisiert. Der Nachteil ist, dass ein Redakteur eine Einstellung mehr vornehmen muss.

Vorgehen bei der Implementierung

Im TCA wird das FAL Feld üblicherweise so definiert (hier am Beispiel image):

'image' => array(
	'exclude' => 1,
	'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang_db.xlf:tx_myextension_domain_model_xxx.image',
	'config' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getFileFieldTCAConfig(
		'image',
		array(
			'maxitems' => 1,
			'minitems' => 1,
		),
		$GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']
	),
),

Nun kann man in der Datei Referenz weitere Felder anzeigen, z.B. Link oder Titel. Warum auch nicht die Sprache? Und so sieht das angepasste TCA aus:

'image' => array(
	'exclude' => 1,
	'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang_db.xlf:tx_myextension_domain_model_xxx.image',
	'config' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getFileFieldTCAConfig(
		'image',
		array(
			'maxitems' => 1,
			'minitems' => 1,
			'foreign_types' => array(
				'0' => array(
					'showitem' => '
								--palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;basicoverlayPalette,
								--palette--;;filePalette, sys_language_uid'
				),
				\TYPO3\CMS\Core\Resource\File::FILETYPE_IMAGE => array(
					'showitem' => '
								--palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;basicoverlayPalette,
								--palette--;;filePalette, sys_language_uid'
				),
			),
		),
		$GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']
	),
),

Dabei wird die Palette basicoverlayPalette eingeblendet (man kann auch imageoverlayPalette einblenden), danach die filePalette und das Feld sys_language_uid. Die filePalette ist eine versteckte Palette und enthält bereits sys_language_uid. Daher ist es wichtig, das Eingabefeld für sys_language_uid nach der filePalette einzublenden. Das Ergebnis im BE sieht man im Bild oben. Und die Bilder der lokalisierten Datensätze werden im FE ausgegeben.

TYPO3 7.6. Templates Form-Extension überschreiben

Seit TYPO3 Version 7 ist die Systemextension Form auf Fluid-Templates umgestellt. Ich habe lange gesucht, wie man nun die Template-Pfade ändert und habe nichts gefunden. In der alten Version der Form-Extension ging es über die Angabe von layout in den Settings. Ich hatte einen Beitrag dazu geschrieben, wie man die Ausgabe an das vom Foundation Framework gewünschte HTML anpasst.

Nun können die Templates wie bei anderen Extensions auch überschrieben werden. Man braucht folgende Konfiguration im Setup:

plugin.tx_form {
	view.partialRootPaths.20 = EXT:my_extension/Resources/Private/Templates/Form/Partials/
	view.templateRootPaths.20 = EXT:my_extension/Resources/Private/Templates/Form/Templates/
}

Man kann den Pfad theoretisch auch in den Konstanten setzen, da die Variable aber direkt ins Setup übernommen wird und ich keine Vererbung erkennen kann, habe ich mich für den Weg entschieden.

Nun braucht man folgende Ordnerstruktur:
EXT:my_extension/Resources/Private/Templates/Form/Partials/Default/Show/FlatElements
d.h. wenn ich z.B. die Ausgabe der Radiobuttons anpassen will, muss ich unter dem genannten Pfad die Datei Radio.html ablegen. Der Pfad zur Datei lautet also vollständig:
EXT:my_extension/Resources/Private/Templates/Form/Partials/Default/Show/FlatElements/Radio.html

Templates Indexed Search (Extbase & Fluid) überschreiben

In der Indexed Search (Extbase & Fluid) sollte es (theoretisch) möglich sein, einen eigenen Pfad zu den Templates anzugeben. Aber so hat es bei mir nicht geklappt:

Folgendes in Konstanten:

plugin.tx_indexedsearch {
	view {
		templateRootPath = EXT:my_extension/Resources/Private/Templates/
		partialRootPath = EXT:my_extension/Resources/Private/Partials/
	}
}

Obwohl der Pfad richtig war und der Wert richtig ins Setup übernommen wurde, zeigte die Angabe keine Wirkung. Meine Vermutung war, dass es mit der neuen Template-Vererbung zu tun hat.

Folgende Angabe im Setup löst das Problem:

plugin.tx_indexedsearch {
	view {
		templateRootPath >
		templateRootPaths {
			0 = EXT:indexed_search/Resources/Private/Templates/
			1 = {$plugin.tx_indexedsearch.view.templateRootPath}
		}
 
		partialRootPath >
		partialRootPaths {
			0 = EXT:indexed_search/Resources/Private/Partials/
			1 = {$plugin.tx_indexedsearch.view.partialRootPath}
		}
	}
}

Erste Erfahrungen mit TYPO3 7.6.

Inspiriert von dem TYPO3 Camp in Essen beschloss ich gleich beim nächsten Projekt TYPO3 7.6. in Kombination mit Fluid Styled Content zu verwenden und sowieso ein paar Dinge anders zu machen als vorher. Am Anfang war es etwas frustrierend, da manche Dinge noch etwas buggy waren oder anders funktionierten als bisher. Zum Beispiel arbeite ich gerne mit Flexforms in Kombination mit Inline-Elementen. Eine Slideshow z.B.: Flexform, Slides als Inline-Datensätze und voilà: einfach für die Redakteure, da alles schön am gleichen Ort ist. Leider funktioniert das Vorgehen mit TYPO3 7.6. nicht (oder zumindest nicht in 7.6.0). Da musste ich auf die klassische Methode ausweichen: Datensätze in Storagefolder und dann auslesen.

Sehr cool ist allerdings Fluid Styled Content. So kann man z.B. eigene Inhaltselemente definieren (Beispiel hier: http://stackoverflow.com/questions/32993534/with-fluid-styled-content-how-to-create-custom-content-elements-in-typo3-7-5) oder das Rendering der Elemente ohne TypoScript nur in Fluid Templates anpassen. Sehr elegant!

Beim Camp wurde auch die Extension mask vorgestellt. Anfang Dezember lief sie noch nicht, aber seit dem Update kurz vor Weihnachten kann man sie auch benutzen. Erster Eindruck: sehr schick! Mit der Extension lassen sich eigene Inhaltselemente definieren (wie bei DCE). Mask erstellt jedoch für diese Inhaltselemente neue Tabellen und Tabellenspalten und (angeblich) bleiben die Inhaltselemente auch erhalten, wenn man Mask deinstalliert. Weiterer grosser Unterschied und Vorteil gegenüber DCE ist, dass man die Elemente mit Mask versionieren kann. Bei DCE ist es zwar möglich die Templates in Dateien auszulagern, aber diese Dateien müssen in fileadmin liegen. Wenn ich also alles in einer Theme-Extension hinterlegt habe, habe ich keine Möglichkeit im Formular im DCE die entsprechenden Templates auszuwählen. Weiterhin müssen in der Entwicklungsumgebung angelegte DCE-Elemente in der Live-Umgebung wieder erstellt werden (gibt es noch andere Möglichkeiten außer SQL-Dump?). Bei Mask wird die Konfiguration in einer json-Datei gespeichert, die Templates werden automatisch aus konfigurierbaren Ordnern ausgelesen. Somit kann man in der Entwicklungsumgebung Mask-Elemente erstellen, dann alles auf den Live-Server spielen, die Mask-Elemente erneut speichern (damit Datenbank aktualisiert wird) und fertig. Habe bisher noch nicht getestet, wie es mit Bildern und Dateien klappt, ansonsten ist Mask bisher sehr vielversprechend.

Fazit: TYPO3 macht Spaß und mit der Version 7.6. noch mehr!

Suchergebnisse indexed_search dynamisch nachladen

Um die Paginierung auszublenden und die Inhalte der nächsten Seiten nachzuladen gibt es für die News-Extension einige Beispiele und sogar ein paar Plugins. Für indexed_search habe ich nichts dergleichen gefunden (Links können gerne in den Kommentaren gepostet werden). Zugegebenermaßen ist es etwas komplizierter als bei News, vor allem deswegen, weil das Template nicht so flexibel ist, aber machbar ist es schon.

Als erstes braucht man eine Seite, die Ajax-Abfragen verarbeitet, das ist für die Suche relativ einfach:

search = PAGE
search {
	typeNum = 1981
	config {
		disableAllHeaderCode = 1
		xhtml_cleaning = none
		admPanel = 0
		metaCharset = utf-8
		additionalHeaders = Content-Type:text/html;charset=utf-8
		disablePrefixComment = 1
	}
	10 < plugin.tx_indexedsearch
}

Nun braucht man einen Button, der bei Klick die Ergebnisse nachlädt. Leider finden sich nicht das komplette HTML der indexed_search im Template. Einiges wird über TypoScript generiert, manche Wrapper werden im PHP-Code einfach gesetzt. Den Button könnte man bestimmt auch mit jQuery generieren, ich habe ihn mittels stdWrap nach den Results eingefügt:

plugin.tx_indexedsearch {
	resultlist_stdWrap.wrap = |<button type="button" class="load-more expand secondary"><i class="fa fa-refresh"></i> {$labelMoreSearchResults}</button>
}

Dann muss man nur noch mit jQuery den Pagebrowser ausblenden und die Ergebnisse bei Klick auf Button nachladen. In diesem Skript sind ein paar Dinge umschön. Zum einen werden bei Indexed Search die Parameter nicht an die Links in der Paginierung angehängt. Bei Klick auf eine Seite wird mit JavaScript ein Parameter im Formular gesetzt und das Suchformular erneut abgeschickt. Im Skript unten mache ich es ähnlich. Zum anderen wird bei der Paginierung am Ende an Link zur nächsten Seiten generiert mit „>>“. Somit sind mehr Links in der Paginierung als Seiten, daher die Abfrage pagesLoaded == paginationPages - 1. Man müsste schauen, ob dieser Code-Schnipsel dadurch ggf. für einzelne Projekte angepasst werden muss.

var pagesLoaded = 0;
var paginationPages;
 
function loadMoreResults() {
	if($('.tx-indexedsearch-browsebox').eq(0).find('ul.browsebox').length < 1) {
		// Button ausblenden wenn keine Paginierung
		$('.tx-indexedsearch-res .load-more').hide();
		return;
	}
	// Paginierung ausblenden
	$('.tx-indexedsearch-browsebox ul.browsebox').hide();
 
	var button = $('.tx-indexedsearch-res .load-more');
 
	var paginationLinks = $('.tx-indexedsearch-browsebox').eq(1).find('ul.browsebox li:not([class])');
	paginationPages = paginationLinks.length;
 
	// Button Event & Ajax-Request
	button.not('.disabled').on('click', function(e){
		e.preventDefault();
 
		if(button.hasClass('disabled')){
			return;
		}
 
		if(pagesLoaded == paginationPages - 1) {
			return;
		}
 
		$('#tx_indexedsearch_pointer').val(pagesLoaded + 1);
		$('#tx_indexedsearch_freeIndexUid').val('-1');
 
		$.ajax({
			async: 'true',
			url: window.location.href + '?type=1981',
			data: $('#tx_indexedsearch').serialize(),
			type: 'POST',
			dataType: 'html',
			success: function (data) {
				pagesLoaded++;
				$data = $(data);
				var insertContent = $data.find('.result-row');
				var insertAfter = $('.tx-indexedsearch-res .result-row').last();
				insertAfter.after(insertContent);
 
				button.removeClass('disabled');
				// last page in pagination is >>, skip that
				if(pagesLoaded == paginationPages - 1) {
					button.remove();
				}
			},
			error: function (error) {
				button.removeClass('disabled');
			}
		});
	});
}
 
$(document).ready(function() {
	loadMoreResults();
});

Geschrieben in typo3 | Kommentare deaktiviert für Suchergebnisse indexed_search dynamisch nachladen