Pikaday

Erstellung eines wirklich responsive Datepickers mit JavaScript

Von am 16.04.2014

Es gibt unzählige JS-basierte Datepicker da draußen. Obwohl grundsätzlich wohl für jeden Geschmack etwas dabei ist, gibt es einen Bereich wo quasi alle Lösungen Schwächen aufweisen: die Benutzung am Smartphone ist meist eher suboptimal.

Pickadate.js, der etablierteste responsive Datepicker, funktioniert zwar am Handy, allerdings – für mich – auch nicht wirklich zufriedenstellend. Das Problem an der ganzen Sache ist, dass die nativen Datepicker der Smartphones ja eigentlich sehr gut funktionieren. Warum also nicht einfach das native HTML5 Input Type “Date” verwenden?

Der native Datepicker unter Android 4.4

Der native Datepicker unter Android 4.4

Warum nicht HTML5 Datepicker verwenden?

Somit stellt sich die Frage, ob die Benutzung der nativen HTML5 Date-Eingabefelder nicht optimal wäre. Dabei kommen jedoch folgende Probleme auf:

  • Selbst wenn man alte Browser aus dem Spiel lässt, ist der Support für <input type=”date” /> leider sehr schlecht. Während Smartphones es sehr gut unterstützen, funktioniert es im Firefox gar nicht und im Chrome bietet der native Datepicker leider eine sehr schlechte User Experience.
  • Das Datumsformat lässt sich bei nativen Datepickern leider nicht anpassen. Laut W3C-Spezifikation soll das Ausgabeformat immer “YYYY-MM-DD” sein, während das Anzeigeformat den Browserherstellern überlassen wird (im deutschen Chrome zB. ist es “DD.MM.YYYY”)

Meine Recherche nach einer wirklich zufriedenstellenden Lösung hat leider kein Ergebnis geliefert – also entschloss ich mich, mich selber daran zu versuchen. Herausgekommen ist ein jQuery Plugin auf Basis von Pikaday, welches ich PikadayResponsive getauft habe. Das fertige Plugin ist auf Github verfügbar.

Die Idee

Der Datepicker soll so funktionieren:

  • Auf Desktop-Geräten kann über den Pikaday-Datepicker das Datum ausgewählt werden.
  • Auf mobilen Touch-Devices kann über den nativen Datepicker das Datum ausgewählt werden.
  • Das Anzeige-Format des Datums kann dabei einheitlich eingestellt werden.
  • Das Ausgabe-Format kann dabei ebenfalls angepasst werden. (Standardmäßig als UNIX-Timestamp)

Mit Ausgabe-Format ist das gemeint, was man beim Abschicken des Formulars dann herausbekommt.

Der Ansatz

PikadayResponsive ist der Einfachheit halber ein jQuery-Plugin geworden. Dieses kann man auf ein Input-Feld anwenden.

Das Input-Feld erhält den type=”hidden”. Es dient nur noch dazu, den Ausgabe-Wert zu enthalten. Nun überprüft das Plugin via Modernizr (oder einer alternativen Feature-Detection Library), ob Touch-Support UND HTML5-Date Support vorhanden sind. Wenn nein, wird ein Input-Field erzeugt und Pikaday darauf angewandt. Wenn ja, wird ein readonly-Feld erzeugt, in welchem dann die Anzeige dargestellt wird. Direkt darüber wird ein zweites Eingabefeld mit type=”date” erzeugt, welches absolut positioniert wird und via CSS und “opacity: 0” gestylt wird.

Wenn man nun auf das Eingabefeld klickt, öffnet sich der native Datepicker. Via “onchange” wird jede Änderung am nativen Datepicker abgefangen und auf Basis dessen ein Datum mit der gewünschten Formatierung in das readonly-Feld geschrieben.

Das ganze klingt komplizierter, als es ist 😉 Eine kleine Demo zeigt, wie das Ganze funktioniert:

http://fnovy.com/projects/PikadayResponsive/

Und am Smartphone:

Die Umsetzung

Der komplette Code ist auf Github verfügbar. Ich werde hier kurz die einzelnen Schritte herzeigen und erklären.

Da das Ganze ein jQuery-Plugin ist, gibt es einige Einstellungen die man wählen kann. Details finden sich auf Github, für hier sind nur die Einstellungen “settings.displayFormat” und “settings.outputFormat” wichtig. Diese können Datumsformate enthalten, welche mit der JS-Library Moment.js geparst werden. Standardmäßig ist displayFormat “DD.MM.YYYY” und outputFormat “unix” (=UNIX Timestamp).

Als Erstes wird das eigentliche Input-Field auf hidden umgestellt und das Ganze in einem Container gewrappt. Der Container ist notwendig, damit das readonly-Overlay absolut positioniert werden kann.

var elem = $(this);
// Original Input-Field is hidden and will contain the final output value
elem.attr("type", "hidden");
// Wrap the input in a container
elem.wrap("<span class='pikaday-container'></span>");
var container = elem.parent(".pikaday-container");

Nun gibt es zwei Möglichkeiten, die ich einzeln erklären werde. Wenn Touch ODER HTML5-Date NICHT verfügbar sind, wird der Pikaday-Picker angezeigt:

var input = $("<input class="pikaday-display" type="text" />");
container.append(input);
var picker = new Pikaday({
    field: $(input)[0],
    format: settings.displayFormat,
});

Nun gibt es ein Input-Field mit Pikaday, jedoch fehlt noch eine korrekte Ausgabe. Wenn das Form abgeschickt wird oder man versucht, den Value mit $(“#date”).val() oder ähnlichem auszulesen, erhält man natürlich keinen Wert, da die Eingabe ja in ein anderes Feld erfolgte. Deshalb achten wir via onchange auf ein Ändern des Wertes des Pikaday-Feldes, und tragen dann entsprechend etwas in das ursprüngliche Eingabefeld ein.

input.change(function() {
	var date = moment($(this).val(), settings.displayFormat);
	var val;

	if (!date.isValid() || date.unix() < 0) {
		// Date is not valid
		input.addClass("is-invalid");
	} else {
		// Date is valid
		input.removeClass("is-invalid");

		// Output format can be unix or a Moment.js string
		if (settings.outputFormat === "unix") {
			val = date.unix();
		} else {
			val = date.format(settings.outputFormat);
		}
		elem.val(val);
	}

	elem.trigger("change");
});

Was passiert hier?

Zuerst wird überprüft, ob das eingegebene Datum valide ist. Wenn nicht, erhält das Ausgabe-Feld eine CSS-Klasse. Wenn das Feld valide ist, wird ein Ausgabewert erstellt – entweder ein UNIX-Timestamp oder ein beliebig formatiertes Datum. Dieser Wert wird dann in das ursprüngliche Element (elem) geschrieben.

Auf Smartphones ist das Ganze grundsätzlich ähnlich. Zuerst werden zwei statt nur einem Input-Feld erzeugt:

// The following element will be read-only and click-through
// and will only be used to display the formatted date
var display = $("<input class="pikaday-display pikaday-display-native" type="text" readonly="readonly" />");
container.append(display);

// The actual input field
var input = $("<input class="pikaday-invisible" type="date" />");
container.append(input);

Ganz wichtig ist hierbei, dass diese via CSS passend gestylt sind. Ein minimal-Style wäre:

.pikaday-container {
	display: inline-block;
	position: relative;
}

/* Height and width has to be equal! */
.pikaday-display, .pikaday-invisible {}

.pikaday-display-native {
	pointer-events: none;
	cursor: pointer;
}

.pikaday-display.is-invalid {
	background: rgba(255,0,0,0.05);
}

.pikaday-invisible {
	opacity: 0;
	position: absolute;
	left: 0;
	top: 0;
}

Die wichtigsten Punkte dabei sind:

  • pikaday-display und pikaday-invisible müssen gleich groß sein
  • pikaday-invisible muss absolut positioniert sein und opacity: 0 haben
  • pikaday-display-native muss pointer-events: none haben, damit man nicht darauf klicken kann.

Die onchange-Funktion für den native Datepicker sieht dabei quasi genauso aus wie die für Pikaday, nur dass zusätzlich noch die Anzeige gesetzt wird:

// ...
display.val(date.format(settings.displayFormat));

Das fertige Script enthält noch ein paar weitere Optionen und Optimierungen. Ich freue mich über Fragen, Anregungen oder Kritik.

1 Kommentar

  • bertram am 23.05.2014 um 14:39

    Tolles Script und auch toll erklärt soweit.
    Danke
    Nur wie kann ich ein voreingestelltes Datum bei Laden der Seite einfügen?

The comments are closed.