Eine Uhr für das TYPO3 Backend

Letztens bin ich über ein Problem gestolpert: Es gibt nicht wirklich eine angenehme Methode die aktuelle Serverzeit im TYPO3 Backend einzusehen. Warum wäre das wichtig? Nun ja, bei terminierten Veröffentlichungen von Seiten und Inhalt wäre es schon hilfreich zu wissen, in welcher Zeitzone das System arbeitet. Grade bei internationalen TYPO3 Instanzen, wo mehrere Benutzer aus unterschiedlichen Ländern Inhalte bearbeiten oder wenn man sich selbst mal in einer anderen Zeitzone befindet, ist es wichtig zu wissen, welche Zeit grade auf dem Server herrscht.

Ich habe mir hierfür überlegt, warum nicht mal eine TYPO3 Erweiterung bauen, welche eine Uhr im Header des TYPO3 Backends integriert. Der Platz ist quasi ungenutzter, ergo verlorener, Raum und würde sich prächtig für eine Uhr eignen.

Also lets go!

Toolbar Implementierung

Um uns ins Backend an der Stelle der Toolbar einhängen zu können, brauchen wir eine Toolbar Klasse, welche das ToolbarItemInterface implementiert:

 

<?php
namespace TheCodingOwl\Oclock\Toolbar;

use TYPO3\CMS\Backend\Toolbar\ToolbarItemInterface;

class Clock implements ToolbarItemInterface {
    /**
     * Checks the access rights to the Clock ToolbarItem
     *
     * @return bool
     */
    public function checkAccess(): bool {
        return TRUE;
    }

    /**
     * Get the DOM for the Clock ToolbarItem
     *
     * @return string
     */
    public function getItem(): string {
        return '';
    }

    /**
     * Checks if the ToolbarItem has a dropdown
     *
     * @return bool
     */
    public function hasDropDown(): bool {
        return TRUE;
    }

    /**
     * Get the DOM for the dropdown
     *
     * @return string
     */
    public function getDropDown(): string {
        return '';
    }

    /**
     * Get an array with additional attributes for the ToolbarItem container
     *
     * @return array
     */
    public function getAdditionalAttributes(): array {
        return [];
    }

    /**
     * Get the index number of the ToolbarItem, basically the position of the item
     * in the toolbar. Lower means further left, higher further right.
     *
     * @return int
     */
    public function getIndex(): int {
        return 0;
    }
}

 

Das Interface gibt uns hier eine komfortable Schnittstelle in die TYPO3 Toolbar und wir können uns auf die Implementierung der nötigen Funktionen konzentrieren.

Zunächst eine kleine Erklärung der einzelnen funktionen und ihrer Bedeutung, bevor wir die Ausgabe konkret implementieren. Die Funktionen erklären sich wie folgt:

  • getIndex: Der Wert dieser Funktion definiert die Positionierung des neuen Toolbar Items. Dabei stehen kleinere Zahlen für eine Positionierung eher links ind er Toolbar, wärend größere Zahlen für eine Positionierung eher Rechts sorgen. In unserem Fall wollen wir möglichst Links mit unserer Uhr stehen und können den Wert 0 so stehen lassen.
  • getAdditionalAttributes: Hier könne wir unserem Toolbar Item Container unsere custom HTML-Attribute mit geben. Dies könnten zum Beispiel data-Attribute sein und wir werden später davon Gebrauch machen.
  • hasDropDown: Diese Funktion definiert, ob das Toolbar Item auch ein DropDown Element anbietet, welches beim Hover über das Item erscheinen soll. Das TYPO3 Cache Toolbar Item bietet zum Beispiel solch ein Dropdown.
  • getDropDown: Hier bauen wir unser Dropdown HTML und geben es zurück. Wir werden später den Inhalt unseres DropDowns für die Server- und Browserzeit hier zusammmen bauen.
  • getItem: Hier wird das HTML des eigentlichen Toolbar Items definiert. Wir werden hier unsere Serverzeit anzeigen.
  • checkAccess: Hier können wir einen Access Check für unser Toolbar Item implementieren. Dies würde zum Beispiel einen Check der Benutzergruppe des eingeloggten Backend Users beinhalten und das Toolbar Item so für bestimmte Gruppen ein- beziehungsweise ausschalten. Ebenfalls könnte hier eine Konfiguration geprüft werden und dementsprechen entschieden werden, ob das Toolbar Item angezeigt wird. Da die Uhr für jeden User zur Verfügung stehen soll, können wir hier einfach TRUE zurück geben.

Item und Dropdown HTML

Nun geht es daran, eine HTML Struktur für unser Item uns dessen Dropdown zu definieren. Für das Item ist dies ein einfaches span-Element, welches später die Zeit aufnimmt und anzeigt.

 

    /**
     * Get the DOM for the Clock ToolbarItem
     *
     * @return string
     */
    public function getItem(): string {
        return '<span class="server-time"></span>';
    }

 

Dieses span-Element werden wir später über ein JavaScript ansprechen und die Zeit einfügen, hier brauchen wir von Serverseite aus nichts zu unternehmen. Da wir allerdings die aktuelle Serverzeit auch irgendwie an den Client, sprich den Browser, kommunizieren müssen, werden wir die Additional Attributes nutzen, um einen Datumsstring auszugeben, welchen wir über unser JavaScript auslesen können.

 

    /**
     * Get an array with additional attributes for the ToolbarItem container
     *
     * @return array
     */
    public function getAdditionalAttributes(): array {
        $currentDateTime = new \DateTime();
        return [
            'class' => 'tx_oclock',
            'data-time' => $currentDateTime->format('r'),
            'data-timezone' => $currentDateTime->format('e')
        ];
    }

 

Wir geben dem ToolbarItem zunächst eine Klasse, damit wir es identifizieren können. Danach lassen wir den Server einmal einen Zeitstring im RFC2822 Format und den Zeitzonenbezeichner als data-Attribute an das Toolbar Item anfügen. Diese Informationen nutzen wir später in einem JavaScript, um die Uhr für den Server zu erzeugen.

Da der Platz in der Toolbar kostbar ist und wir ihn nicht unnötig verbrauchen wollen, rendern wir uns die Informationen zu Zeitzone und eine Browserzeit in einem Dropdown raus. Das HTML definieren wir wie folgt:

 

    /**
     * Get the DOM for the dropdown
     *
     * @return string
     */
    public function getDropDown(): string {
        return '<p>' . $this->languageService->sL('LLL:EXT:oclock/Resources/Private/Language/locallang.xlf:toolbar.timezone.server') . ': <span  class="server-timezone">' . (new \DateTime())->format('e') . '</span></p>'
            . '<p>' . $this->languageService->sL('LLL:EXT:oclock/Resources/Private/Language/locallang.xlf:toolbar.time.server') . ': <span class="server-time">' . '</span></p>'
            . '<hr />'
            . '<p>' . $this->languageService->sL('LLL:EXT:oclock/Resources/Private/Language/locallang.xlf:toolbar.timezone.browser') . ': <span class="browser-timezone"></span></p>'
            . '<p>' . $this->languageService->sL('LLL:EXT:oclock/Resources/Private/Language/locallang.xlf:toolbar.time.browser') . ': <span class="browser-time"></span></p>';
    }

 

Generell ist dies keine besondere Struktur, sondern einfach ein paar Paragraphen mit span-Elemente, welche besondere Klassen zugewiesen bekommen haben, damit wir diese im JavaScript ansprechen und mit den Informationen des Servers und des Browsers befüllen können.
Eine kleine Besonderheit ist in diesem Fall die Nutzung des Language Service, welchen wir vorher als Klassenvariable definieren. Für das initialisieren solcher Services eignet sich der Konstruktor gut.

 

use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Localization\LanguageService;

class Clock implements ToolbarItemInterface {
    /**
     * @var LanguageService
     */
    protected $languageService;

    /**
     * Constructs the Clock toolbar item
     */
    public function __construct() {
        $this->languageService = GeneralUtility::makeInstance(LanguageService::class);
    }

 

Somit können wir den LanguageService überall in der Klasse benutzen.

Um dieses neue ToolbarItem nun auch anzuzeigen, müssen wir dem TYPO3 Backend in einer Konfigurationsvariablen sagen, dass es sie gibt. Dies bewerkstelligen wir in der ext_localconf.php indem wir in $GLOBALS['TYPO3_CONF_VARS']['BE']['toolbarItems'] den Klassennamen unseres ToolbarItems hinzufügen:

 

<?php

call_user_func(function($extKey) {
    $GLOBALS['TYPO3_CONF_VARS']['BE']['toolbarItems'][] = \TheCodingOwl\Oclock\Toolbar\Clock::class;
}, 'oclock');

 

Nach einem CacheClear im TYPO3 Install Tool, sollte das Backend unser ToolbarItem nun finden und auh anzeigen. Wir sehen zwar nur die HTML Struktur, aber diese füllen wir im nächsten Schritt mit Leben.

JavaScript Modul Implementierung

Um dem ToolbarItem nun Leben einzuhauchen, registrieren wir uns ein kleines requireJS Modul im TYPO3 Backend, welches die HTML Struktur des ToolbarItems aufgreift und die übergebenen Daten der Serverzeit in eine tickende Uhr umwandelt.

Die Registrierung des requireJS Moduls fügen wir einfach in den Konstruktor unserer ToolbarItem Klasse ein. Dafür initialisieren wir uns den PageRenderer und nutzen dessen Funktionen zum Registrieren von requireJS Modulen.

 

    /**
     * Constructs the Clock toolbar item
     */
    public function __construct() {
        $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
        $pageRenderer->loadRequireJsModule('TYPO3/CMS/Oclock/Luxon');
        $pageRenderer->loadRequireJsModule('TYPO3/CMS/Oclock/Clock');
        $this->languageService = GeneralUtility::makeInstance(LanguageService::class);
    }

 

Zunächst eine Erklärung, wie TYPO3 requireJS Module verwaltet. Der Namespace des Moduls entspricht folgendem Schema:

  • TYPO3/CMS/: Dieser Teil ist quasi vorgegeben und repräsentiert ein Modul in der TYPO3 Architektur
  • Oclock: Dieser Teil stellt den Namen der Extension dar. In diesem Fall ist das die Extension oclock, aber man könnte hier auch beliebige Module aus zum Beispiel der Kernextension backend laden, indem man einfach Backend eingibt.
  • Clock: Dies ist das JavaScript Modul. Im Grunde entspricht dies dem Namen einer JavaScript Datei im Extension Ordner Resources/Public/JavaScript. In diesem Fall würde also die Datei Resoures/Public/JavaScript/Clock.js geladen.

Wir laden hier zusätzlich eine Bibliothek, welche eine bessere DateTime Implementierung in JavaScript bietet, da JavaScript von Haus aus nicht mit verschiedenen Zeitzonen arbeiten kann. (https://moment.github.io/luxon/)

Unser Clock Modul fangen wir zunächst als requireJS Modul an:

 

define(['jquery', 'TYPO3/CMS/Oclock/Luxon'], function($, luxon){
    let Clock = {};
    return Clock;
});

 

Da jquery im TYPO3 Backend ohnehin zur Verfügung steht, machen wir davon auch Gebrauch. Zusätzlich laden wir schonmal unsere DateTime Bibliothek, welche wir später zum Erzeugen der Daten benutzen werden. Wir bereiten uns zunächst ein Objekt für unsere Clock vor. Ich habe es mir angewöhnt meine Selektoren in dieses Objekt zu schreiben, damit ich sie an einer zentralen Stelle pflegen kann und nicht mühselig danach suchen muss, wenn sich die Selektoren einmal ändern sollten.

 

define(['jquery', 'TYPO3/CMS/Oclock/Luxon'], function($, luxon){
    let Clock = {
        selector: '.tx_oclock',
        serverTimeSelector: '.server-time',
        browserTimeSelector: '.browser-time',
        serverTimeZoneSelector: '.server-timezone',
        browserTimeZoneSelector: '.browser-timezone'
    }:
    return Clock;
});

 

Die Selektoren entsprechen denjenigen, welche wir in der ToolbarItem Klasse definiert haben.

In einer init-Funktion sorge ich jetzt für das Initialisieren der Uhr.

 

        /**
         * Initialize the Clock
         */
        init: function() {
            let clock = this,
                containers = $(clock.selector);
            /**
             * Run through the containers of the oclock extension in the backend
             */
            containers.each(function(){
                let container = $(this),
                    date = luxon.DateTime.fromRFC2822(
                        container.data('time'),
                        { zone: container.data('timezone') }
                    );
                clock.setTimeZone(date, container.find(clock.serverTimeZoneSelector));
            });
        },

 

In dieser init-Funktion durchlafen wir zunächst alle Container. welche dem Selektor entsprechen. Damit initialisieren wir die Clock an jeder Stelle, an der sie eventuell vorkommen könnte. Wir haben zunächst zwar nur ein Vorkommen, aber es ist einfacher, sie jetzt in dieser Weise zu initialisieren, als später den Code mühselig anpassen zu müssen, wenn man die Clock in einem anderen Modul in der selben Weise einbauen möchte.

Innerhalb der Container-loop initialisieren wir uns nun ein Luxon DateTime Objekt, welches das RFC2822 Datum des Servers, welches wir in der ToolbarItem Klasse als data-Attribut ausgegeben haben, als Vorlage nimmt. Mit diesem Server Time Objekt können wir nun die Serverzeit an die Stellen ausgeben, welche den Server Time Selektoren entsprechen. Zunächst jedoch geben wir die Zeitzone des Servers aus. Da wir auch die Zeitzone des Browsers an späterer Stelle im Dropdown ausgeben wollen, nutzen wir hier eine allgemeine Methode dafür.

 

        /**
         * Set the timezone string to the given element
         *
         * @param {DateTime} date The object containing the Date
         * @param {jQuery} element The jquery object of the element to append the timezone string to
         */
        setTimeZone: function(date, element) {
            element.text(date.offsetNameLong);
        }

 

Das Luxon DateTime Objekt bietet hierfür eine komfortable Methode, mit der wir die Zeitzone in lesbarer Form ausgeben können. Die Zeitzone wird in diesem Objekt als "Offset" bezeichnet und es gibt auch Methoden um die Kurzform dieser ausgeben zu können. Wir geben den lesbaren Offset in kompletter Form aus und nutzen daher offsetNameLong.

Nun widmen wir uns der Ausgabe der Serverzeit. Dafür gehen wir wieder alle Elemente durch, an welche die Serverzeit gerendert werden soll. Wir haben nun in der Tat zwei Stellen, an denen diese Zeit ausgegeben werden muss, daher nutzen wir die foreach-Variante der Container auch hier.

 

        /**
         * Initialize the Clock
         */
        init: function() {
            let clock = this,
                containers = $(clock.selector);
            /**
             * Run through the containers of the oclock extension in the backend
             */
            containers.each(function(){
                let container = $(this),
                    elements = container.find(clock.serverTimeSelector),
                    date = luxon.DateTime.fromRFC2822(
                        container.data('time'),
                        { zone: container.data('timezone') }
                    );
                clock.setTimeZone(date, container.find(clock.serverTimeZoneSelector));
                /**
                 * Run through the elements that contain the timers
                 */
                elements.each(function() {
                    let element = $(this),
                        spans = clock.createTimeSpans()
                        // need to create the time twice, so seconds are not added multiple times when updating times
                        date = luxon.DateTime.fromRFC2822(
                            container.data('time'),
                            { zone: container.data('timezone') }
                        );
                    clock.updateTime(date, spans);
                    clock.appendTimes(element, spans);
                    clock.setClock(date, spans);
                });
            });
        },

 

Innerhalb der Element Initialisierung erstellen wir uns zunächst ein paar span-Elemente, welche die Zeit aufnehmen können. Da wir eine tickende Uhr bauen wollen, ist es Ratsam, wenn wir Stunde, Minute und Sekunde separat ändern können.

 

        /**
         * Create the object with the spans containing the times
         *
         * @return {object}
         */
        createTimeSpans: function() {
            let spans = {
                hour: document.createElement('SPAN'),
                minute: document.createElement('SPAN'),
                second: document.createElement('SPAN')
            };
            spans.hour.classList.add('hour');
            spans.minute.classList.add('minute');
            spans.second.classList.add('second');
            return spans;
        },

 

Die spans füllen wir nun mit der Zeit. Da wir dies nach jeder Aktualisierung der Zeit wieder machen müssen, kreieren wir uns auch hierfür eine eigene Funktion.

 

        /**
         * Update the time spans
         *
         * @param {DateTime} date The object that contains the date
         * @param {object} spans An object containing the spans that contain the times
         */
        updateTime: function(date, spans) {
            let hours = date.hour,
                minutes = date.minute,
                seconds = date.second;
            spans.hour.innerText = hours < 10 ? '0' + hours : hours;
            spans.minute.innerText = minutes < 10 ? '0' + minutes : minutes;
            spans.second.innerText = seconds < 10 ? '0' + seconds : seconds;
        },

 

Falls die Stunden/Minuten/Sekunden jeweils einstellig sein sollten, fügen wir eine führende Null an. Dies ist einfach nur dazu da, damit die Zeitanzeige besser aussieht.

Die span-Elemente platzieren wir nun an ihrem Platz.

 

        /**
         * Appends the times to the given element
         *
         * @param {jQuery} element The jquery object of the element to append the times to
         * @param {object} spans An object containing the spans that contain the times
         */
        appendTimes: function(element, spans) {
            element.append(spans.hour);
            element.append(document.createTextNode(':'));
            element.append(spans.minute);
            element.append(document.createTextNode(':'));
            element.append(spans.second);
        },

 

Voilà, die Zeit wird in unserer Toolbar angezeigt. Um eine funktionierende Uhr zu erhalten, müssen wir diese jedoch jede Sekunde aktualisieren. Dafür initialisieren wir uns eine Interval Funktion, welche die Zeiten aktualisiert und damit quasi das Herzstück unserer Uhr darstellt.

 

        /**
         * Start the actual "clock", this means this starts the interval that updates the time
         *
         * @param {DateTime} date The object containing the Date
         * @param {object} spans An object containing the spans that contain the times
         */
        setClock: function(date, spans) {
            let clock = this;
            setInterval(function(){
                date = date.plus({seconds: 1});
                clock.updateTime(date, spans);
            }, 1000);
        },

 

Die Serverzeit funktioniert hiermit bereits, allerdings werden wir zusätzlich eine Browserzeit anzeigen, damit der User weis, ob sein Browser eine abweichende Zeit anzeigt, als der Server verwendet. Vom Prinzip her müssen wir die Aktualisierungs- und Initialisierungsroutine der Serverzeit nur mit einem DateTime Objekt des Browsers wiederholen.

 

        /**
         * Initialize the Clock
         */
        init: function() {
            let clock = this,
                containers = $(clock.selector);
            /**
             * Run through the containers of the oclock extension in the backend
             */
            containers.each(function(){
                let container = $(this),
                    elements = container.find(clock.serverTimeSelector),
                    date = luxon.DateTime.fromRFC2822(
                        container.data('time'),
                        { zone: container.data('timezone') }
                    );
                clock.setTimeZone(date, container.find(clock.serverTimeZoneSelector));
                /**
                 * Run through the elements that contain the timers
                 */
                elements.each(function() {
                    let element = $(this),
                        spans = clock.createTimeSpans()
                        // need to create the time twice, so seconds are not added multiple times when updating times
                        date = luxon.DateTime.fromRFC2822(
                            container.data('time'),
                            { zone: container.data('timezone') }
                        );
                    clock.updateTime(date, spans);
                    clock.appendTimes(element, spans);
                    clock.setClock(date, spans);
                });
                let browserElements = container.find(clock.browserTimeSelector);
                date = luxon.DateTime.local();
                clock.setTimeZone(date, container.find(clock.browserTimeZoneSelector));
                browserElements.each(function () {
                    let element = $(this),
                        spans = clock.createTimeSpans()
                        date = luxon.DateTime.local();
                    clock.updateTime(date, spans);
                    clock.appendTimes(element, spans);
                    clock.setClock(date, spans);
                });
            });
        },

 

Mittels luxon.DateTime.local() erstellen wir uns ein DateTime Objekt der Browserzeit und wiederholen den Aufruf unserer Funktionen mit den entsprechenden Selektoren, welche die Browserzeit span-Elemente selektieren.

Die init-Funktion müssen wir nun noch aufrufen und schon haben wir eine funktionierende Anzeige der Server- und Browserzeit im Backend. Das vollständige JavaScript Modul sieht dann volgendermaßen aus:

 

define(['jquery', 'TYPO3/CMS/Oclock/Luxon'], function($, luxon){
    let Clock = {
        selector: '.tx_oclock',
        serverTimeSelector: '.server-time',
        browserTimeSelector: '.browser-time',
        serverTimeZoneSelector: '.server-timezone',
        browserTimeZoneSelector: '.browser-timezone',
        /**
         * Initialize the Clock
         */
        init: function() {
            let clock = this,
                containers = $(clock.selector);
            /**
             * Run through the containers of the oclock extension in the backend
             */
            containers.each(function(){
                let container = $(this),
                    elements = container.find(clock.serverTimeSelector),
                    date = luxon.DateTime.fromRFC2822(
                        container.data('time'),
                        { zone: container.data('timezone') }
                    );
                clock.setTimeZone(date, container.find(clock.serverTimeZoneSelector));
                /**
                 * Run through the elements that contain the timers
                 */
                elements.each(function() {
                    let element = $(this),
                        spans = clock.createTimeSpans()
                        // need to create the time twice, so seconds are not added multiple times when updating times
                        date = luxon.DateTime.fromRFC2822(
                            container.data('time'),
                            { zone: container.data('timezone') }
                        );
                    clock.updateTime(date, spans);
                    clock.appendTimes(element, spans);
                    clock.setClock(date, spans);
                });
                let browserElements = container.find(clock.browserTimeSelector);
                date = luxon.DateTime.local();
                clock.setTimeZone(date, container.find(clock.browserTimeZoneSelector));
                browserElements.each(function () {
                    let element = $(this),
                        spans = clock.createTimeSpans()
                        date = luxon.DateTime.local();
                    clock.updateTime(date, spans);
                    clock.appendTimes(element, spans);
                    clock.setClock(date, spans);
                });
            });
        },
        /**
         * Update the time spans
         *
         * @param {DateTime} date The object that contains the date
         * @param {object} spans An object containing the spans that contain the times
         */
        updateTime: function(date, spans) {
            let hours = date.hour,
                minutes = date.minute,
                seconds = date.second;
            spans.hour.innerText = hours < 10 ? '0' + hours : hours;
            spans.minute.innerText = minutes < 10 ? '0' + minutes : minutes;
            spans.second.innerText = seconds < 10 ? '0' + seconds : seconds;
        },
        /**
         * Create the object with the spans containing the times
         *
         * @return {object}
         */
        createTimeSpans: function() {
            let spans = {
                hour: document.createElement('SPAN'),
                minute: document.createElement('SPAN'),
                second: document.createElement('SPAN')
            };
            spans.hour.classList.add('hour');
            spans.minute.classList.add('minute');
            spans.second.classList.add('second');
            return spans;
        },
        /**
         * Start the actual "clock", this means this starts the interval that updates the time
         *
         * @param {DateTime} date The object containing the Date
         * @param {object} spans An object containing the spans that contain the times
         */
        setClock: function(date, spans) {
            let clock = this;
            setInterval(function(){
                date = date.plus({seconds: 1});
                clock.updateTime(date, spans);
            }, 1000);
        },
        /**
         * Appends the times to the given element
         *
         * @param {jQuery} element The jquery object of the element to append the times to
         * @param {object} spans An object containing the spans that contain the times
         */
        appendTimes: function(element, spans) {
            element.append(spans.hour);
            element.append(document.createTextNode(':'));
            element.append(spans.minute);
            element.append(document.createTextNode(':'));
            element.append(spans.second);
        },
        /**
         * Set the timezone string to the given element
         *
         * @param {DateTime} date The object containing the Date
         * @param {jQuery} element The jquery object of the element to append the timezone string to
         */
        setTimeZone: function(date, element) {
            element.text(date.offsetNameLong);
        }
    };
    Clock.init();
    return Clock;
});

 

Fazit

Das Erstellen eines Backend Toolbar Item ist, dank des zur Verfügung stehenden Interfaces, kinderleicht und kleine Funktionen und Hilfestellungen lassen sich schnell integrieren. Wer ein komplexeres Element implementieren möchte, sollte vielleicht ein Fluid Template für die Ausgabe des HTML nutzen, anstatt ihn so wie in diesem kleinen Beispiel von Hand in die PHP-Klasse zu schreiben. Bei größeren Strukturen geht die Übersicht ansonsten relativ schnell verloren.

Happy coding ;)