TYPO3 Neos Inhaltselement «Text mit Bild» an Twitter Bootstrap anpassen

Das fantastische an TYPO3 Neos ist, dass ohne dreckige Hacks bestehende Inhaltselemente modifiziert werden können . Jede Ausgabe kann genau so gesteuert werden, wie es gewünscht wird.

In diesem Artikel sollte es um das Inhaltselement «Text mit Bild» gehen. Dabei haben wir folgende Vorgaben:

  1. Ausrichtungen: Links, rechts, oben und unten
  2. Die Positionierung sollte mit den CSS Klassen von Twitter Bootstrap gemacht werden
  3. Die CSS Klassen sollten einfach veränderbar sein
  4. Wenn möglich sollte im Quelltext der Text vor dem Bild positioniert sein, damit in der Mobile-Ansicht der Text vor dem Bild erscheint
  5. Um ein effektives Styling zu ermöglichen, sollte im umschliessenden Tag schon eine Klasse mit der Auszeichnung der Positionierung enthalten sein
  6. Das HTML für das Rendering des Bildes sollte nicht neu definiert werden müssen
  7. Damit es wiederverwendbar wird, sollte das ganze in ein eigenes Plugin gepackt werden

Ordner- und Filestruktur

Als erstes legen wir folgende Ordner- und Filestruktur an:

 Dotpulse.TextWithImage
        | composer.json
        |
        | – Configuration
        |       | NodeTypes.yaml
        |       | Settings.yaml
        |
        | – Resources
                |
                | – Private
                      |
                      | – Templates
                      |      | TextWithImage.html
                      |
                      | – TypoScript
                             | Root.ts2 

Konfiguration

In der Datei composer.json werden allgemeine Informationen und ggf. Abhängigkeiten vom Plugin angegeben:

 {
    "name": "dotpulse/textwithimage",
    "type": "typo3-flow-plugin",
    "description": "Styling the content element 'Text with Images' with Bootstrap",
    "require": {
        "typo3/flow": "*"
    },
    "autoload": {
        "psr-0": {
            "Dotpulse\\TextWithImage": "Classes"
        }
    }
}

Durch den Autload Befehl wird das Plugin später automatisch geladen.

In der Datei NodeTypes.yaml konfigurieren wir das Standardverhalten. Dabei ändern wir nicht den NodeType TextWithImage sondern ändern das Mixin, welches der NodeType zum SuperType hat. Somit erbt nicht nur dieses Inhaltselement die Eigenschaften, sondern auch andere Elemente, welche dieses Mixin nutzen.

# Overwrite image alignment mixin
'TYPO3.Neos.NodeTypes:ImageAlignmentMixin':
  properties:
    'alignment':
      ui:
        label: 'Position'
        inspector:
          editorOptions:
            values:
              top:
                label: 'Top'
              bottom:
                label: 'Bottom'
              center: ~

'TYPO3.Neos.NodeTypes:Image'
  properties:
    'alignment': []

Da sich das Wort Alignment mehr auf die Positionierung auf einer Linie bezieht, ändern wir das Label auf Position. Als nächstes erstellen wir die zusätzlichen Werte top und bottom. Den Wert center leeren wir, da wir dies nicht anbieten wollen. Am Schluss entfernen wir beim Inhaltselement Bild die Auswahl vom Alignment, da wir dies dort nicht benötigen.

In die Datei Settings.yaml kommt folgender Inhalt:

TYPO3:
  Neos:
    typoScript:
      autoInclude:
        'Dotpulse.TextWithImage': TRUE

Dotpulse:
  TextWithImage:
    defaultAlignment: 'right'
    left:
      text: 'text col-md-8 col-md-push-4'
      image: 'image col-md-4 col-md-pull-8'
    right:
      text: 'text col-md-8'
      image: 'image col-md-4'
    top:
      text: 'text'
      image: 'image'
    bottom:
      text: 'text'
      image: 'image'

Durch die erste Anweisung wird der folgende TypoScript 2 Code automatisch geladen. Im zweiten Abschnitt werden die CSS Klassen sowie die Standardeinstellungen definiert. Dadurch lässt sich später das Plugin extrem einfach konfigurieren, ohne das TypoScript oder Templates angefasst werden müssen.

TypoScript

Die Root.ts2 Datei wird durch den autoInclude-Befehl in der Settings.yaml geladen. Mit TypoScript lesen wir die Settings aus und übergeben all diese Variabeln an das Fluid-Template.

Als erstes sagen wir Neos, dass wir das Element «Text with Image» bearbeiten möchten:

prototype(TYPO3.Neos.NodeTypes:TextWithImage) {
	
}

Da wir diverse Änderungen am Template vornehmen müssen, geben wir als erstes eine neue Template Datei an:

templatePath = 'resource://Dotpulse.TextWithImage/Private/Templates/TextWithImage.html'

Wir wollen aber nicht den kompletten Image-Tag neu schreiben (Siehe Anforderung #6), sondern wir wollen, dass Neos weiterhin das Standardrendering verwendet:

partialRootPath = 'resource://TYPO3.Neos.NodeTypes/Private/Templates/NodeTypes/Partials'

Als nächstes lesen wir unsere YAML-Konfiguration aus:

configuration = ${Configuration.setting('Dotpulse.TextWithImage')}

Da wir den Konfigurations-Array auch innerhalb von dem Attribute-Objekt benötigen, speichern wir die Konfiguration als eigene Variable:

@override.configuration = ${this.configuration}

Nun lesen wir die Position des Bildes aus. Falls kein Wert im Inspektor gewählt wurde, soll der Standardwert aus der Settings.yaml genommen werden. Auch diesen Wert benötigen wir wieder als eigene Variable:

alignment = ${q(node).property('alignment') ? q(node).property('alignment') : configuration.defaultAlignment}
@override.alignment = ${this.alignment}

Falls die Ausrichtung links oder rechts ist, wird ein umschliessendes div mit der Klasse row benötigt:

needRow = ${alignment == 'left' || alignment == 'right' ? 'row' : ''}
@override.needRow = ${this.needRow}

Da wir nun wissen, wie die Ausrichtung ist, speichern wir die Klassen für das Fluid Template in einer Variabel ab:

colClasses = ${configuration[alignment]}

Falls das Element direkt auf der Seite integriert wurde (und nicht innerhalb eines anderen Inhaltselement), wollen wir dem umschliessenden Tag noch die CSS-Klasse container geben. Da wir dies auch im Fluid-Template wissen müssen, speichern wir es für das Template wie auch als Variabel für die Attribute ab:

container = ${q(node).parent().parent().is('[instanceof TYPO3.Neos:Document]')}
@override.container ? ${this.container}

Um ein bisschen Fluid-Markup zu sparen, fragen wir noch die Kombination needRow und container ab:

isContainerAndNeedRow = ${needRow && container ? TRUE : FALSE}

Als letztes setzen wir die CSS Attribute für den umschliessenden Div-Tag. Die CSS Klasse pos-alignment kann später helfen, per CSS ein spezielles Styling für die verschiedenen Positionen zu erstellen.

attributes.class = TYPO3.TypoScript:RawArray {
	container = ${container ? 'container' : needRow ? 'row' : ''}
	alignment = ${'pos-' + alignment}
}

Unser fertiger TypoScript Code:

prototype(TYPO3.Neos.NodeTypes:TextWithImage) {
	// Template Datei
	templatePath = 'resource://Dotpulse.TextWithImage/Private/Templates/TextWithImage.html'

	// Setzte den Pfad zu den Partials
	partialRootPath = 'resource://TYPO3.Neos.NodeTypes/Private/Templates/NodeTypes/Partials'

	// Lesen der Konfiguration
	configuration = ${Configuration.setting('Dotpulse.TextWithImage')}
	@override.configuration = ${this.configuration}

	// Bestimmen der Position
	alignment = ${q(node).property('alignment') ? q(node).property('alignment') : configuration.defaultAlignment}
	@override.alignment = ${this.alignment}

	// Falls die Ausrichtung links oder rechts ist, benötigt es .row
	needRow = ${alignment == 'left' || alignment == 'right' ? 'row' : ''}
	@override.needRow = ${this.needRow}

	// Bestimmen der CSS Klassen
	colClasses = ${configuration[alignment]}

	// Wurde das Element direkt auf der Seite integriert?
	container = ${q(node).parent().parent().is('[instanceof TYPO3.Neos:Document]')}
	@override.container ? ${this.container}

	// Kombination von container und needRow
	isContainerAndNeedRow = ${needRow && container ? TRUE : FALSE}


	// Setzen der Attribute
	attributes.class = TYPO3.TypoScript:RawArray {
		container = ${container ? 'container' : needRow ? 'row' : ''}
		alignment = ${'pos-' + alignment}
	}
}

Template

Wir haben’s bald geschafft! Nun fehlt nur noch das Fluid-Template: TextWithImage.html.
Da wir im Template ein Neos-Tag (für den Text) benötigen, binden wir dessen Namespace ein:

{namespace neos=TYPO3\Neos\ViewHelpers}

Als erstes definieren wir die umschliessenden Tags. Falls das Inhaltselement direkt auf der Seite eingefügt wurde und links oder rechts positioniert ist, wird es mit einem zusätzlichen Tag umschlossen.

<div{attributes -> f:format.raw()}>
	<f:if condition="{isContainerAndNeedRow}">
		<f:then>
			<div class="{needRow}">
				<f:render section="Item" arguments="{_all}" />
			</div>
		</f:then>
		<f:else>
			<f:render section="Item" arguments="{_all}" />
		</f:else>
	</f:if>
</div>

Es gibt ein einziger Fall bei der Positionierung, bei der das Markup sich von den Anderen unterscheidet: Falls das Bild oben ausgerichtet wird, kommt selbst im Quelltext das Bild zuerst. In allen anderen Fällen wird immer zuerst der Text ausgegeben. Die Darstellung, in der das Bild links vom Text ist, wird mit den Push- und Pull-Klassen umgesetzt.

<f:section name="Item">
	<f:if condition="{alignment} == 'top'">
		<f:then>
			<f:render section="Image" arguments="{_all}" />
			<f:render section="Text" arguments="{_all}" />
		</f:then>
		<f:else>
			<f:render section="Text" arguments="{_all}" />
			<f:render section="Image" arguments="{_all}" />
		</f:else>
	</f:if>
</f:section>

Als letztes legen wir noch fest, wie die Sections «Image» und «Text» gerendert werden:

<f:section name="Image">
	<div class="{colClasses.image}">
		<f:render partial="Image" arguments="{_all}" />
	</div>
</f:section>

<f:section name="Text">
	<neos:contentElement.editable property="text" class="{colClasses.text}" />
</f:section>

Unser fertiges HTML-Template:

{namespace neos=TYPO3\Neos\ViewHelpers}
<div{attributes -> f:format.raw()}>
	<f:if condition="{isContainerAndNeedRow}">
		<f:then>
			<div class="{needRow}">
				<f:render section="Item" arguments="{_all}" />
			</div>
		</f:then>
		<f:else>
			<f:render section="Item" arguments="{_all}" />
		</f:else>
	</f:if>
</div>

<f:section name="Item">
	<f:if condition="{alignment} == 'top'">
		<f:then>
			<f:render section="Image" arguments="{_all}" />
			<f:render section="Text" arguments="{_all}" />
		</f:then>
		<f:else>
			<f:render section="Text" arguments="{_all}" />
			<f:render section="Image" arguments="{_all}" />
		</f:else>
	</f:if>
</f:section>

<f:section name="Image">
	<div class="{colClasses.image}">
		<f:render partial="Image" arguments="{_all}" />
	</div>
</f:section>

<f:section name="Text">
	<neos:contentElement.editable property="text" class="{colClasses.text}" />
</f:section>

All dies in den Ordner gepackt und in den Ordner «Plugins» geladen ergibt dem Benutzer die Möglichkeit, die Position vom Bild komfortabel im Inspektor zu wählen.

Download

Wem die ganze Abschreibarbeit zu mühsam ist, kann das Plugin hier downloaden.