
Start bis 4.10.2017

Frontend-Editing mit TYPO3 - Ein Zwischenbericht
Frontend-Editing mit TYPO3 ist überschaubar, wenn man einige Paradigmen ignoriert.
- Warte nicht auf den Server => Rufe Extbase-Actions per AJAX auf.
- Bilde die Relationen der Datenbank als Partial-Aufrufe ab - Keine Angst vorm Partial im Partial im Partial .... Aufruf!
- Jede Modell hat ein Partial, dass eine Ausgabe, ein Eingabeformular und einen Testbereich enthält.
- Frontend-Editing braucht ein globales Menü, dass hunderte Menü-Icons ein- und ausblenden kann, mit denen dann das jeweilige Formular aktiviert wird.
- Nutze Scope-basiertes JavaScript und generische IDs.
- Alle Modells nutzen für ihre Input/Output-Aktionen das gleiche Frontend-Editing-Pattern/Konzept.
Der nachfolgende Text ist nur in Deutsch verfügbar und beschreibt das Projekt 'Positioner' im Detail. Die Extension 'positioner' ist aktuell in der Pilotphase und auf www.düddelei.de im (Satire-)Einsatz. (Deutsche Satire, also eher unlustig und garstig).
Die neue Version 8.1.0 ist noch nicht veröffentlicht, wobei die 8 in der versionsnummer aus der Lauffähigkeit unter TYPO3 8.7.1 resultiert. Die Version ist nicht 8.0, weil die Version nicht unter TYPO 7.6 lauffähig ist.
Langsam, weil ...
Aktuell entwickelt sich die Extension nur langsam, da ich die Extension, den SVG-Viewhelper und mein modulares Konzept vom Frontend-Editings leider nur in meiner Freizeit weiterentwickeln kann, obwohl SVG' eigentlich viel Potential im Internet hätte. (Warum Shopbetreiber und Websitebetreiber kaum SVGs für die Präsentation ihrer Produkte und für ihre Teaser nutzen, erschließt sich mir nicht wirklich. Mit SVG könnte man eigentlich schön, einfach und flexibel Texte, Preise, Bild und Graphik variantenreich zu Teasern und Anzeigen kombinieren kann. Dazu muss man SVG nur nicht als Graphik sondern vielmehr als schnell anpassbares und austauschbares Template begreifen. SVG ist für mich nur eine Erweiterung von HTM mit ein paar graphischen Zusatzeigenschaften.
P.S. Warnung. Mir hat kürzlich ein Designer SVG-Code von einem bekannten Profi-Pixelgraphikprogramm gezeigt. Der SVG-Code für die Textpfade war von ähnlich 'Qualität' wie der HTML-Code eines weltbekannten Textverarbeitungsprogramms. Bedienungsfehler durch den Designer möchte ich nicht ausschließen; vermute aber schlechte Programmierung als Ursache für den gesehenen SVG-Code. Ich sage dies nur als Warnung, dass nicht jeder SVG-Code gleichermaßen gut als Template geeignet ist. Für guten SVG-Code würde ich derzeit InkScape für die Template-Vorbereitung und einen Texteditor für die Nachbereitung empfehlen.
Comics für das Internet
Frontend-Editing mit TYPO3 in normalisierten Datenbank-Domänen
Murphy: „In jedem großen Problem steckt ein Kleines, das raus will.“ Bei Comics und Cartoons gilt das umgekehrte. Eigentlich sind Comics und Cartoons gehören zu Klasse der Bild-Text-Kompositionen. Sie sollten sich einfach fürs Internet bauen lassen. Diese Bild-Text-Klasse durchdringt unser Leben; Werbeplakate, Katalogseiten, Möbelbauanleitungen, Straßenkarten oder auch chemischen Reaktionsgleichungen gehören dazu. Weil TYPO3 bei Pixelgraphiken nicht zwischen Karikatur, Bauzeichnung und Werbefoto unterscheidet, sollte ein Comic-Extension sehr vielfältig einsetzbar sein oder als Templategenerator dienen. In meiner Naivität übersah ich ein großes Problem, das dem Problemchen Internet-Comic innewohnt: die Datenflut.
Der Artikel ist in drei Teile aufgeteilt. Das erste Drittel nähert sich der Extension aus konzeptioneller Sicht. Das zweite Drittel beschäftigt sich mit Code-Ausschnitten und der Integration des Frontend-Editings-Pattern. Das letzte Drittel reflektiert den aktuelln Stand, analysiert Vor- und Nachteile und wagt einen Zukunftsblick.
Konzept: Zuviel ist niemals gut
Da TYPO3 7.6 und auch nicht TYPO3 8.6 keine Konzept anbietet, musste ich selbst ein Frontend-Editing-Konzept erdenken. Es sollte schnell, einfach und standardisiert sein. Performance war mir nicht wichtig. Nach mühevollen Wegwerfen alter Ideen wuchs irgendwann die simple Idee, die Prinzipien der Datenbank-Normalisierung systematisch auch für Views und Controller zu fordern. In der Comic-Extension repräsentiert jedes Modell ein Objekt, dass einen View, Tests, ein Inputformular und einen Controller sein eigen nennt. Wie ein normalisierter Datensatz ist ein normalisierte Objekt vollständig einzigartig im Vergleich zum Rest der Welt und gleichzeitig doch nur ein Rädchen in der Komposition 'Comic'. Die Abbildung 1 zeigt, was ich alles für einen Comic für wichtig hielt. Das XML-Konstrukt beschreibt gleichzeitig auch die grundsätzliche HTML-Struktur.
<story> <artinfo /> <comic dimension borderstyle > <artinfo /> <cartoon dimension position borderstyle rotation > <picture dimension image /> <speech position dimension rotation > <bubble color symmetry image overlaysBox /> <bubblepointer dimension position color symmetry rotation image / … > </bubble> <text /> <audio audiofiles /> </speech … > </cartoon … > </artinfo> </comic> </artinfo> </story>
Wie das XML-Schema zeigt, muss neben den Texten und Bildern viel zusätzlich definiert werden. Dazu gehören zum Beispiel Größe, Ausrichtung, Symmetrie, Position und Farbraum für jede Sprechblase. Angesichts der vielen Größendefinitionen wird die Trennung zwischen Backend und Frontend, die man in TYPO3 und vielen anderen Content-Management-Systemen findet, zum Problem. Der Redakteur sieht im Backend nicht mehr, was er im Frontend eigentlich anrichtet. Für Comics brauche ich also WYSIWYG (What You se, is what you get).
Das WYSIWYG muss dem Redakteur schnell Änderungen erlauben. Dies funktioniert nur mit AJAX, dass für die Weiterarbeit nicht auf die Serverrückmeldung wartet. Die Abbildung 1 impliziert auch verschiedene Relationen zwischen Tabellen/Models. Im View können die Relation durch Partialaufrufe übersichtlich nachgebildet. Für die Übersichtlichkeit wird eine hohe Verschachtelungstiefe in Kauf genommen. In der php.ini musste zum Beispiel der Default-Wert xdebug.max_nesting_level=100 auf 1200 hochgesetzt werden. Wegen der Übersichtlichkeit werden auch im View auch Dateneingabe, Datenausgabe und Tests zusammengehalten, wobei hier View meist eine Komposition von Partialaufrufen meint. Damit nun ein Surfer keine Änderungen am comic vornehmen kann, programmierte ich den Burka-Viewhelper. Dieser zeigt seine umschlossenen Formulare und Tests nur dann an, wenn ich gleichzeitig als Admin eingeloggt bin. Die TYPO3-Viewhelper f:security.ifAuthenticated und f:security.hasRole gelten nur für Frontend-User und lassen erahnen, wie stark das Paradigma der Frontend-Backend-Trennung TYPO3 durchdringt und mich bei der Ideenfindung behinderte.
Paradigmen: Hemmschuhe gegen neue Ideen
Die Entwicklung des Frontend-Editing erfolgte sehr kleinschrittig und führte mich nach vielen Irr- und Umwegen zum Frontend-Editing mit Icon-Menüs. Dafür waren verschiedene Arbeiten nötig:
Der Viewhelper f:form ist fehlerhaft und musste durch einen eigenen form-Viewhelper gepatcht werden. (siehe BUG #80341 im TYPO3-forge)
Ich brauchte generische ID's für Menüs, Formulare und Datenfelder. Gegen den Geist vom MVC werden diese aktuell vom View generiert und sind nicht im Model definiert.
Für das einfache Bearbeiten von Position, Größe etc waren jQuery-Plugin-Lösungen einzubauen.
Die Extension Positioner erlauben dem User, für Cartoons und Comic individuell CSS-Rahmenklassen auszuwählen. Diese Klassen selbst werden in der Extension „borderstyle“ definiert. Die Extension war ein Umweg, um zu prüfen, wie man mit TYPO3 temporäre Style-Themes definieren könnte. (Die Extension ermöglicht es zum Beispiel Inhalte zu Ostern oder zu Weihnachten mit besonderen individuell definierbaren Rahmen zu schmücken.)
Jedes Dateneintrag kann erst verändert werden, wenn das dazugehörige Formular per Klick auf einen Icon/Text-Button aktiviert wurde. Jede Deaktivierung führt automatisch beim Wechsel zu anderen Formularen oder beim Submit-Event. Bei Tear-Down wird automatisch der letzte Eintrag gespeichert.
Die Buttons für Create- oder Delete-Action aktivieren immer einen Seitenreaload. Da alle Icons verschiebbar sein sollten, muss für die Auslösung der Aktion beim Anklicken die Taste C(reate) oder D(elete) gedrückt sein.
Leider beißen sich die Syntax von Fluid und Javascript, so dass solide Funktionstest mit Jasmine nur wenig und wenn kompliziert zu realisieren waren.
Leider funktioniert aus irgendeinem Grund die Sprachumschaltung nicht korrekt. Die Ursache ist unklar. (siehe TYPO3-Bug 57272)
Design-Pattern für modulares Frontend-Editing
Ein Kernmerkmal der Extension 'positioner' ist für jedes Modell genutzte Frontend-Editing-Pattern. Das Pattern spiegelt sich als Querschnitt-Aufgabe an vielen Stellen der Extension wider. Die Code-Fragmente für das normalisierte Objekt 'rotation' sind in der Extension über seinen Namen bestimmbar.
Das Objekts 'rotation' braucht natürlich Definitionen für Referenz und Modell in der Datenbank. Entsprechender Code findet sich in der Datei ext_tables.sql
Abbildung. Datenbank-Definitionen in ext_tables.sql
CREATE TABLE tx_positioner_domain_model_cartoon ( ... rotation int(11) unsigned DEFAULT '0', ... ) CREATE TABLE tx_positioner_domain_model_rotation ( ...)
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addLLrefForTCAdescr( 'tx_positioner_domain_model_rotation', 'EXT:positioner/Rotation/Private/Language/locallang_csh_tx_positioner_domain_model_rotation.xlf' ); \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::allowTableOnStandardPages( 'tx_positioner_domain_model_rotation' );
Abbildung.generische Namen für Actions in ext_localconf.php
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin( 'Porth.' . $_EXTKEY, 'Cartoon', array( .... 'Rotation' => 'ajax, createrotationcartoon, updaterotationcartoon, deleterotationcartoon,...', .. ), // non-cacheable actions array( ... 'Rotation' => 'ajax, createrotationcartoon, updaterotationcartoon, deleterotationcartoon,...', .. ) );
Abbildung.Create-Methode mit Dummy-Methode aus Repository
/** * Creates a new rotation-entry * * @param \Porth\Positioner\Domain\Model\Cartoon $parent The blog the post belongs to * @param \Porth\Positioner\Domain\Model\Rotation|null $rotation A fresh Post object which has not yet been persisted * @return void */ public function createrotationcartoonAction( \Porth\Positioner\Domain\Model\Cartoon $parent, \Porth\Positioner\Domain\Model\Rotation $rotation = null ) { // see the comment in BubbleController in method createbubblespeechAction /** @var \Porth\Positioner\Domain\Model\Cartoon $myParent */ $myParent = $this->cartoonRepository->findByUid($parent->getUid()); if (is_null($myParent->getRotation())){ $rotation = $this->rotationRepository->buildDummy($this->storagePid,\Porth\Positioner\Domain\Repository\RotationRepository::ROTATIONREPOSITORY_FLAG_DUMMY_CARTOON); $this->rotationRepository->add($rotation); $myParent->setRotation($rotation); } $this->cartoonRepository->update($myParent); $this->persistenceManager->persistAll(); $uri = self::detectUriToCurrentPageWithoutTypeNum(); $this->redirectToUri($uri); }
Abbildung.Dummy-Methode definiert sinnvolle Startwerte
class RotationRepository extends \TYPO3\CMS\Extbase\Persistence\Repository { public function buildDummy($pid = 0, $type = self::ROTATIONREPOSITORY_FLAG_DUMMY_CARTOON) { /** @var \Porth\Positioner\Domain\Model\Rotation $dummy */ $dummy = $this->objectManager->get("Porth\\Positioner\\Domain\\Model\\Rotation"); $dummy->setPid($pid); switch (ucfirst($type)) { case self::ROTATIONREPOSITORY_FLAG_DUMMY_CARTOON: $dummy->setRotation(0); break; .... } self::rebuildDummyElement($dummy, $type); return $dummy; } }
Abbildung.Zusätzliche reset-Methode in Elter-Modell
/** * Cartoon */ class Cartoon extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity { … public function resetRotation() { $this->rotation = null; } } Natürlich finden sich auch TypoScript-Definitionen für das Pattern. Hier Konstanten definiert, die man braucht um dynamisch, flexibel und systematisch das Menü und die Formulare definieren und steuern zu können. Abbildung. TypoScript-Definitionen zur Steuerung von Menu und Formularen plugin.tx_positioner.settings.controller { ... action { type { update = update delete = delete create = create } parent { ... rotation { name { cartoon = cartoon speech = speech bubblepointer = bubblepointer } parts { create = create delete = delete all = all rotation = rotation } } } } menu { action { update = update delete = delete create = create } dataTypeAdditional = -datatype dataType { ... rotation = rotation } dataReaction { ... rotation = rotation create = create delete = delete } modelAdditional = -objecttype model { ... rotation = rotation } parentAdditional = -parenttype parent { ... cartoon = cartoon } flag { redirect = redirect } } }
Da sich das Pattern 'rotation' von vielen anderen Pattern abgrenzen soll, finden sich in der CSS-Datei der Extension abgrenzende (Farb-)Definitionen. Da das JavaScript nur vom Backend genutzt wird, ist es nur wenig optimiert. Das JavaScript für das normalisierte Objekt 'rotation' ist sehr ähnlich mit dem Pattern für 'dimension', 'position' oder andere. Bei Aktivieren wird ein SetUp-Prozess gestartet, der das Formular des Model in das Menü verschiebt, der den Button als Aktiv kennzeichnet und und der gegebenenfalls ein jQuery-Plugin aktiviert. Beim Tear-Down-Prozess wird der Submit-Event gestartet und danach alles wieder zurückgebaut. War die Ajax-Übertragung erfolgreich, wird im Info-Tab des Menü eine Grün-Meldung intwegriert. War sie fehlerhaft, kann man sich gegebenenfalls die TYPO3-Fehlermeldung anzeigen lassen.
Um den Code möglichst allgemeingültig zu halten, startet der SetUp-Prozess durch Anclicken des Buttons. Der Button enthält in data-Attributen alles Wichtige, wie Formular-IDs und Feld-IDs. Da manches DOM-Element mehrere Daten- bzw. Feldeinträge enthalten kann bzw. weil Manche Daten an mehreren Stellen zu setzen sind, werden die DOM-Elemente der Daten enthaltenden Felder über $('div[data-fieldid*=",' + destination + ',"]') eindeutig bestimmt. Damit die CSS-Suche greift, im Template von TYPO3 darauf geachtet werden, dass ein Eintrag für das Attribut data-fieldid immer durch Kommas eingeschlossen ist.
Abbildung. Auszug aus dem JavaScript für Frontend-Editing
/*********************************************************************************************** * * Rotation * the destination is binded to the jQueryUi-resize-Acrtion, which know the event 'resizestop' */ porth.positioner.FrontendEditing.setUpUserTransformJsValueToCssValue = function (javaScriptValue) { return parseInt(((360 + javaScriptValue * 180 / Math.PI) % 360), 10); }; porth.positioner.FrontendEditing.setUpUserTransformCssValueToJsValue = function (inputFormValue) { return parseInt((inputFormValue / 180 * Math.PI), 10); }; porth.positioner.FrontendEditing.setUpUserStopChangeRotationBind = function ($destination, $source, jsSourceArray) { var params = { angle: porth.positioner.FrontendEditing.setUpUserTransformCssValueToJsValue($source.val()), wheelRotate: true, stop: function (event, ui) { var radValue, degreeValue; radValue = porth.positioner.FrontendEditing.utility.extraktValueFromUi(ui, jsSourceArray[porth.positioner.FrontendEditing.params.jQueryResource]); degreeValue = porth.positioner.FrontendEditing.setUpUserTransformJsValueToCssValue(radValue); porth.positioner.FrontendEditing.utility.setForm($source, degreeValue); } }; if ($destination.data('flag-formhelper-rotation') != '1') { // make container rotable $destination.rotatable(params); // flag for unique activation/deactivation of ration-helper $destination.data('flag-formhelper-rotation', '1'); } }; /** * fill the input-form by the jQuery-Plugin * * @param $destination */ porth.positioner.FrontendEditing.setUpUserStopChangeRotationFree = function ($destination) { if ($destination.data('flag-formhelper-rotation') == '1') { $destination.rotatable('destroy'); // flag for unique activation/deactivation of ration-helper $destination.data('flag-formhelper-rotation', '0'); } }; /** * parametrisize the jQuery-Plugin by changing the input-form * * @param $destination */ porth.positioner.FrontendEditing.setUpUserStopChangeRotationReBind = function ($destination, $source, jsSourceArray) { $source.on('focusout', function () { $destination.rotatable( 'angle', porth.positioner.FrontendEditing.setUpUserTransformCssValueToJsValue($(this).val()) ); }); }; porth.positioner.FrontendEditing.setUpBindingRotation = function (source, destination, jsSourceArray) { // init blinking of active object var $destination = $('div[data-fieldid*=",' + destination + ',"]'), $source = $('#' + source); porth.positioner.FrontendEditing.blinkBackgroundDataId($destination, true); porth.positioner.FrontendEditing.setUpUserStopChangeRotationBind($destination, $source, jsSourceArray); porth.positioner.FrontendEditing.setUpUserStopChangeRotationReBind($destination, $source, jsSourceArray); }; porth.positioner.FrontendEditing.tearDownBindingRotation = function (source, destination) { var $destination = $('div[data-fieldid*=",' + destination + ',"]'), $source = $('#' + source); // stop blinking of active object and disallwo resizing porth.positioner.FrontendEditing.blinkBackgroundDataId($destination, false); porth.positioner.FrontendEditing.setUpUserStopChangeRotationFree($destination); $source.off('focusout'); }; porth.positioner.FrontendEditing.setUpTypeRotation = function ($posMarkup) { var pairAryRaw = $posMarkup.data('pairs'), pairAry = (!!pairAryRaw) ? pairAryRaw.split(';') : '', formId = $posMarkup.data('form'), i, pair, source, destination, jsSourceArray; // porth.positioner.FrontendEditing.triggerLastSaveRequestBeforeTearDown(); //move form to selectLayer porth.positioner.FrontendEditing.removeFormAndClassInOldActiveButton((pairAry !== ''), false); $('#' + formId).appendTo('#' + porth.positioner.FrontendEditing.constants.selectLayerEditId); for (i = 0; i < pairAry.length; i++) { // get Names of field and place pair = pairAry[i].split(','); source = pair[porth.positioner.FrontendEditing.constants.positionFormFieldId]; destination = pair[porth.positioner.FrontendEditing.constants.positionViewId]; jsSourceArray = porth.positioner.FrontendEditing.utilityExtraktParameters(porth.positioner.FrontendEditing.constants.positionJavaScriptValue, pair); if (jsSourceArray.length >0) { porth.positioner.FrontendEditing.setUpBindingRotation(source, destination, jsSourceArray); } } $posMarkup.addClass('active'); }; porth.positioner.FrontendEditing.tearDownTypeRotation = function ($posMarkup) { var pairAryRaw = $posMarkup.data('pairs'), pairAry = (!!pairAryRaw) ? pairAryRaw.split(';') : '', i, pair, source, destination, jsSourceArray; porth.positioner.FrontendEditing.triggerLastSaveRequestBeforeTearDown(); //remove form selectLayer to original .positioner-markup-Place porth.positioner.FrontendEditing.removeFormAndClassInOldActiveButton(true, true); for (i = 0; i < pairAry.length; i++) { // get Names of field and place pair = pairAry[i].split(','); source = pair[porth.positioner.FrontendEditing.constants.positionFormFieldId]; destination = pair[porth.positioner.FrontendEditing.constants.positionViewId]; jsSourceArray = porth.positioner.FrontendEditing.utilityExtraktParameters(porth.positioner.FrontendEditing.constants.positionJavaScriptValue, pair); porth.positioner.FrontendEditing.tearDownBindingRotation(source, destination); } };
<f:section name="main"> <f:alias map="{ ridRotation: '{rid}_rotation-{cartoon.rotation.uid}',... }“ > <f:alias map="{ … }"> <f:alias map="{ … }" > <porth:burka status="admin_be"> <f:if condition="{cartoon.rotation}"> <f:then> <f:render partial="Forms/Rotation/Cartoon/Delete" … /> </f:then> <f:else> <f:render partial="Forms/Rotation/Cartoon/Create" … /> </f:else> </f:if> </porth:burka> <div style="position:absolute; width:100%; height: 100%; "> <div class="rotation" style=" transform-origin: 50% 50%; transform: rotate({cartoon.rotation.rotation}deg); width: 100%; height: 100%; position:absolute; overflow: hidden;" {porth:burka(status:'admin_be',data:'data-fieldid=",{ridRotationRotation}," id="{ridRotationRotation}"')} > <f:render partial="Cartoon/Picture" … /> </div> <!-- … --> </div> <porth:burka status="admin_be"> <f:if condition="{cartoon.rotation}"> <f:then> <f:render partial="Menu/MarkerSign" … /> <f:render partial="Forms/Rotation" … /> </f:then> </f:if> </porth:burka> </f:alias> </f:alias> </f:alias> </f:section>
<?xml version="1.0"?> <html xmlns:f="http://typo3.org/ns/TYPO3/Fluid/ViewHelpers" xmlns:porth="http://typo3.org/ns/Porth/Positioner/ViewHelpers" > <head> </head> <body> <f:section name="main"> <f:alias map="{ ridDelete: '{rid}_cartoon-delete', ridDeleteMarkup: '{rid}_cartoon-delete-markup', ridDeleteForm: '{rid}_cartoon-delete-form', actionType: settings.controller.action.type.delete, actionFields: settings.controller.action.parent.cartoon.parts.all, actionParent: settings.controller.action.parent.cartoon.name.comic, menuAction: settings.controller.menu.action.delete, menuModel: settings.controller.menu.model.cartoon, menuParent: settings.controller.menu.parent.comic }" > <f:comment><!-- ########################################################################### tests ########################################################################### --></f:comment> <f:comment><![CDATA[<!-- <porth:burka status="admin_be"> <f:if condition="{settings.tests.general.formAspects}"> <div style="display:none">There is noting to test </div> </f:if> </porth:burka> -->]]></f:comment> <f:comment><!-- ########################################################################### content ########################################################################### there no infos to show --></f:comment> <f:comment><!-- ########################################################################### frontend-editing ########################################################################### --></f:comment> <porth:burka status="admin_be"> <f:render partial="Menu/MarkerSign" section="main" arguments="{ rid: ridDelete, ridMarkup: ridDeleteMarkup, ridForm: ridDeleteForm, pairs: 'nothing', menuAction: menuAction, menuModel: menuModel, menuParent: menuParent, menuDataType: settings.controller.menu.dataType.cartoon, menuDataReaction: settings.controller.menu.dataReaction.delete, sizeClass: sizeClass, markerSignStyle: '', sign: '{menuModel}-{menuAction}#{actionParent}', iconPath: '' menuTitle: '{f:translate(key:\'LLL:EXT:positioner/Resources/Private/Language/locallang_editing.xlf:frontend.menu.title.button.info.{menuAction}\')}' }" /> <f:render partial="Forms/Cartoon" section="main" arguments="{ ridForm: ridDeleteForm, cartoon: cartoon, cartoonFields: cartoonFields, actionType: actionType, actionParent: actionParent, actionFields: actionFields, mediaIn: mediaIn, plugIn: plugIn, loadtype: settings.frontendEditing.redirectLoadType, infoText: { error: '{menuModel}-{cartoonFields.all}', success: '{menuModel}-{cartoonFields.all}' }, parent: parent }" /> </porth:burka> </f:alias> </f:section> </body> </html>
<?xml version="1.0"?> <html xmlns:f="http://typo3.org/ns/TYPO3/Fluid/ViewHelpers" xmlns:porth="http://typo3.org/ns/Porth/Positioner/ViewHelpers" > <head> </head> <body> <f:section name="main"> <f:render partial="Forms/Cartoon/Make" section="main" arguments="{ ridForm: ridForm, cartoon: cartoon, cartoonFields: cartoonFields, actionType: actionType, actionParent: actionParent, actionFields: actionFields, menuDataReaction: menuDataReaction, loadtype: loadtype, infoText: infoText, mediaIn: mediaIn, plugIn: plugIn, parent: parent }" /> </f:section> </body> </html>das eigentliche Formular<?xml version="1.0"?> <html xmlns:f="http://typo3.org/ns/TYPO3/Fluid/ViewHelpers" xmlns:porth="http://typo3.org/ns/Porth/Positioner/ViewHelpers" > <head> </head> <f:section name="main"> <div class="hidden" id="{ridForm}-container"> <f:render partial="Forms/General/AjaxError" section="main" arguments="{ ridForm: ridForm, alternativHeadline: '', alternativMessage: '', errorInfo: '.{menuModel}', errorInfoPart: '.{actionFields}'}" /> <porth:form method="post" controller="Cartoon" action="{actionType}{actionFields}{actionParent}" pageType="{f:if(condition:'{loadtype}=={settings.frontendEditing.ajaxLoadType}',then:'{settings.frontendEditing.ajaxTypeNum}',else:'{settings.frontendEditing.redirectTypeNum}')}" arguments="{parent:parent}" additionalParams="{switcher:plugIn}" pluginName="{plugIn}" name="cartoon" object="{cartoon}" id="{ridForm}" class="[ js-form-fe ]" additionalAttributes="{ data-error: '{f:translate(key:\'LLL:EXT:positioner/Resources/Private/Language/locallang_backend.xlf:tx_positioner_domain_model_cartoon.update.error\')} {infoText.error}', data-success: '{f:translate(key:\'LLL:EXT:positioner/Resources/Private/Language/locallang_backend.xlf:tx_positioner_domain_model_cartoon.update.success\')} {infoText.success}', data-action-reload: '{f:translate(key:\'LLL:EXT:positioner/Resources/Private/Language/locallang_backend.xlf:tx_positioner_domain_model_cartoon.question.{actionType}\')}', data-actiontype: '{actionType}', data-type: menuDataReaction }" > <f:render partial="Forms/Cartoon/Fields" section="main" arguments="{ ridForm: ridForm, cartoon: cartoon, cartoonFields: cartoonFields, actionFields: actionFields, mediaIn: mediaIn }" /> <table> <tr> <td> <f:form.submit value="{f:translate(key:'LLL:EXT:positioner/Resources/Private/Language/locallang_editing.xlf:frontend.edit.button.general.{actionType}')}" class="submit" data="{loadtype:'{loadtype}'}" /> </td> <td> <f:form.button type="reset" name="cancelForm" value="cancelForm" class="cancel-form" > <f:translate key="LLL:EXT:positioner/Resources/Private/Language/locallang_editing.xlf:frontend.edit.button.general.cancelForm" /> </f:form.button> </td> </tr> </table> </porth:form> <f:render partial="Forms/Cartoon/JavaScript" section="main" arguments="{ ridForm: ridForm, cartoonFields: cartoonFields, actionFields: actionFields }" /> </div> </f:section> </html>
Bilder sagen mehr als 1000 Worte
Abbildung - Informations-Overkill: Darstellung, wenn ich als Admin eingeloggt bin und alle aktuell verfügbaren 'Icons' „freigezogen“ wurde und wenn die Testumgebung sowie ein Formular aktiv sind.

Abbildung - Zeige an, was du brauchst. Nutze das Frontend-Menü. Nur die 'Rotation' ist gerade ausgewählt.
Abbildung - Und so sieht es am Ende der Surfer.
Abbildung - HTML plus SVG plus 0px bie top, bottom, left & right = scharfe dynamische Sprechblase
<div class=".." style=" position:relative; width:200px;; height:200px;;"> <div style="position:absolute; width:100%; height: 100%; "> <div class=".." style=" transform-origin: 50% 50%; transform: rotate(70deg); width: 100%; height: 100%; position:absolute; overflow: hidden;"> <div class=".." style=" "> <img id=".." style="width:100%; height: auto;" src="/fileadmin/_processed_/8/2/csm_img_0641_f030594dd4.jpg" alt="" height="150" width="200"> </div> </div> <div class=".." style="width: initial; height:0px; overflow: visible; position:absolute; z-index:7; left:111%; top:12%; "> <div class="ui-widget-content [ rotation ]" style="position: relative; width:auto; height:auto; transform-origin: 50% 50%; transform: rotate(30deg);"> <div class=".. " data-svg-container="1" style="position: absolute; display: block; top: 0px; bottom: 0px; left: 0px; right: 0px; filter: alpha(100); opacity:1; ;width: 100%; height: 100%;" title="Alles Cool"> <div class="nix" style="position:absolute; top:0; left:0; transform-origin: 0% 0%; width:100%; height: 100%; z-index: 7;"> <svg id=".." class=".." ... viewBox="0 0 640 480" data-url=".." style="width: 100%; height: 100%;transform-origin: 50% 50%;" preserveAspectRatio="none"> <defs id="defs3350"> <metadata id="metadata3353"> </metadata> <g id="layer1"> <path id="path3357" style="fill-rule: evenodd; stroke-width: 1px; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1;" d="..."> </g> </svg> </div> </div> <div class="..." style="position: relative; z-index: 14; height:auto; width:auto; padding:10% 8% 10% 5% ; " data-text-over-svg="1"> <div style=" height:117%; color: #0014F6;"><p> <span style="font-family: Normalschrift,serif;">Die Lösung war wohl zu stark.</span>. </p></div> </div> <div class="js-resource"> </div> </div> </div> </div> </div>
Zwischenbilanz
Projekt: Reflektion zum aktuellen Stand (April 2017)
Das Konzept des normalisierten MVCs macht die Programmierung übersichtlich und erlaubt ein flexibles Anpassen des Frontend-Editings an beliebige Modelle. Dabei ist TYPO3, abgesehen von Viewhelper-Bug, leicht darauf anzupassen. Eine Portierung des Pattern in den Extensionbuilder ist sicher denkbar.
Um während der Entwicklung die JavaScript-Event-Hölle für mich in Schach halten zu können, brauchte ich im Frontend Unit-Tests mit Jasmine. Grundsätzlich könnte zukünftig das Einbinden von Frontend-Tests helfen, das Updaten von TYPO3-Versionen sicher zu machen und das Testen von TYPO3-Websites zu systematisieren. Ich werde die Funktionstest-Option aus der Extension heraus ziehen und in eine externe Extension auslagern, wenn ich mal irgendwann Zeit dafür finde.
Gleiches gilt für den Ausbau der schon im TER verfügbaren Extension borderstyle, mit welcher Redakteure im Backend Rahmenklassen definieren können, die sie in das Frontwend einbauen könnten – vorausgesetzt ihr Template lässt dies zu. Ich werde in Zukunft das Zeit-Konzept anpassen, um so zum Beispiel tageszeitlich sich ändernde Layouts zu ermöglichen. Inhaltlich müsste die Extension um CSS für Farbgebungen, Schriftstyles, Hintergründe und Animationen erweitert werden. Insbesondere sollte die Extension das einfache Austauschen von Bild-Hintergründen erleichtern. Um TYPO3 echt Themes-fähig zu machen, muss zukünftig eine Zuordnung von mehreren Klassen in einem Klassenfeld möglich sein. Wenn TYPO3 Themes-fähig wäre, könnten Designer und Website-Betreiber darüber nachdenken, an welchen Stellen Redakteuren das Website-Schmücken erlaubt ist/sein muss.
Die Comic-Extension 'positoner' ist für mich eine Lern- & Spiel-Extension. Für die Hintergründe von den Sprechblasen verwende ich SVGs und habe viel über SVGs gelernt. Ich denke, dass das direkte Rendern von SVG's mit TYPO3 viele Seiten aufpeppen könnten, da man mit SVG zum Beispiel die Laufrichtung von Texten frei verändern kann. Vielleicht überkommt es mich und ich baue die Sprechblase noch vor einer Veröffentlichung im TER um? Der nächste Schritt besteht in jedem Fall in eine Test-Installation auf meiner aktuell verwaisten Website www.buergerstimmen.de / www.düddelei.de, um beim Pilot-Betrieb mit politischen Karikaturen die Nutzbarkeit der Extension zu optimieren. An Interessenten verkaufe ich gern die Nutzungslizenzen einer Vorabversion; denn die rund 700 Stunden meiner Freizeit, die ich in das Projekt bislang investiert habe, sollen sich ja zumindest ein wenig lohnen. In zirka ein bis zwei Jahren, wenn ich die Kinderkrankheiten ausgemerzt habe, werde ich die Extension dann im TER veröffentlichen. Mit dem hier veröffentlichten Angaben sollte ein guter TYPO3-Entwickler in 100-200 Stunden eine gleichwertige Extension nachbauen können bzw. auf andere Frontend-Szenarien übertragen. Die grundsätzliche Prinzip ist einfach und lässt asich auch auf andere Frameworks übertragen.
Aktuelle Probleme (Juni 2017) - TYPO3 und die kastrierten SVG#s
Ursprünglich wollte ich mal eben die Extension, die ursprünglich auf TYPO3 7.6 entwickelt wurde, nach TYPO3 8.7 migrieren. Dies stieß auf verschiedene kleiner Probleme. Ein Problem beschäftigt mich aktuell intensiver: die dynamische Manipulation von SVG-Graphiken mit einem Viewhelper.
Alte SVG-Lösung unter TYPO3 7.6
In der ursprünglichen Version wählte ich die Guttenberg-Programmier-Technik und kopierte mir alles zusammen. Die SVG-Graphik wurde damals mit dem f:uri-Viewhelper eingebunden, per JavaScript nachgeladen und in den HTML-Code geschrieben. Das JavaScript übernahm auch die notwendigen Anpassungen, damit über modifizierte SVG-Attribute die Sprechblasen die zugewiesenen Farben annahmen.
SVG-Kastration durch TYPO3 8.7
Unter Typo3 8.78 funktioniert dies nicht mehr, weil der f:uri-Viewhelper die SVG-Graphik über ImageMagick in eine Pixelgraphik umwandelt wurde. Der Code der Pixelgraphik wurde zeichencodiert über das Image-Tag ins SVG zurückgeschmuggelt und alle anderen SVG-Tags wurden entfernt. Diese kastrierte Version des SVG legte TYPO3 dann im Cache ab. Der gesamte SVG-Code fehlt also jetzt und ein nachträgliches Einfärben war mit dem alten (schlechten) Verfahren nicht mehr möglich.
(Eine Zeitlang habe ich versucht, mit Hilfe vom GIF-Builder und TypoScript Pixelgraphiken ähnlich wie beim Positioner 1.0 umzufärben. Aber die mehrfachen Anforderungen [Schutz der transparenten Bereichen, Spiegelung der Bilddateien & Mehrfarbiges Umfärben] habe ich nicht hingefrickelt bekommen. Außerdem haben SVG-Graphiken mehr Charme, weshalb ich mir beim Frickeln nicht allzu viel Mühe gab.)
Angestrebte SVG-Lösung unter TYPO3 8.7
Mit Hilfe eines Viewhelpers soll der SVG-Text direkt in das HTML eingebunden werden. Dabei soll der Viewhelper potentielle Gefahrenstellen für Cross-Site-Scripting per Default entfernen können und dynamisch steuerbare Ersetzungen von Attributen, Texten und Tags auf einfache Weise erlauben. Dabei wird eine grundsätzliche Lösung angestrebt, um zum Beispiel auch für Shop mal eben popige variantenreiche Teaser zu erlauben. Da ich bislang wenig Erfahrungen mit PHP-Unit-Tests habe, nutze ich dies schön abgegrenzte Problem gleich zum Üben, was die Entwicklung zusätzlich verlangsamt. Deshalb sind auf der Pilotseite www.düddelei.de bislang die Sprechblasen schwärzlich.