JavaScript/Tutorials/Eigene modale Dialogfenster
Manchmal benötigt man in einer Webanwendung ein Dialogfenster, um Benutzereingaben für einen ganz speziellen Zweck einzuholen. In diesem Tutorial soll es darum gehen, wie man dieses auf zugängliche Art und mit größter gestalterischer Freiheit erreichen kann.
Siehe auch: HTML/Tutorials/dialog
Inhaltsverzeichnis
Historische Altlasten
In JavaScript existieren die window
-Methoden alert, confirm und prompt, um Benutzereingaben mit einer Dialog-Box einzufordern. Diese Methoden haben ihre historische Berechtigung, jedoch halten Sie jegliche weitere (eventuell auch im Hintergrund laufende) Funktionalität von JavaScript an und versetzen den Browser in einen besonderen Modus, was wiederum dazu führt, dass die eigentliche Seite nicht mehr erreicht werden kann, so lange das modale Fenster angezeigt wird.
Schauen wir uns einmal an, wie man diese drei Methoden so umsetzen kann, dass diese nicht nur frei gestaltbar werden, sondern auch die Ausführung von JavaScript im Hintergrund nicht weiter blockieren.
Nichtlinearer Programmverlauf
Wir müssen uns von dem sehr bequemen Gedanken lösen, dass wir an beliebiger Stelle einen Wert vom Benutzer einholen können. Klassisch würde man so denken:
function irgendwas () {
var alter = 0, minderjaehrig = false;
// Benutzer nach Alter fragen
alter = prompt("Wie alt sind Sie?");
if (alter < 18) {
minderjaehrig = confirm("Du bist also noch nicht erwachsen?");
}
}
Wir müssen im Hinterkopf behalten, dass der Browser alles das, was er tun kann, sofort tut, um dann zu warten, bis er wieder etwas tun kann. Für Benutzereingaben bedeutet das, dass er warten muss, bis der Benutzer eine Eingabe tätigt. Wenn nun aber unser Programm nicht warten kann, da es sofort und gänzlich ausgeführt wird, müssen wir seine Struktur so anpassen, dass die Dinge, die erst zu einem späteren Zeitpunkt ausgeführt werden können, eben nicht sofort ausgeführt werden. Diese packen wir in eine Funktion, die den weiteren Programmablauf enthält und dann ausgeführt werden kann, wenn es soweit ist:
function start () {
var alter = 0, minderjaehrig = false;
// hier werden wir nichtlinear
frage("Wie alt sind Sie?", weiter); // weiter ist eine Funktion s.u.
}
function frage (text, callback, braucheBool) {
// Benutzer etwas fragen
var alter;
if (braucheBool) {
alter = confirm(text); // das werden wir ersetzen
} else {
alter = prompt(text); // das werden wir ersetzen
}
callback(alter); // hier geht es mit Ergebnis weiter
}
function weiter (alter) {
if (alter < 18) {
frage(
"Du bist also noch nicht erwachsen?",
// hier definieren wir die callback-Funktion direkt
function (minderjaehrig) {
ende(alter, minderjaehrig);
},
true // benutze confirm anstelle von prompt
);
} else {
ende(alter, false);
}
}
function ende (alter, minderjaehrig) {
// wir haben alter und minderjaehrig abgefragt
alert(
"Alter: " + alter + " ("
+ (minderjaehrig ? "" : "aber nicht ")
+ "minderjährig)"
);
}
// Programm starten
start();
window
-Methoden mit anderen Techniken ersetzen.Dialog-Boxen
Im Tutorial zugängliche Dialog-Box wird beschrieben, wie man sich das Dialog-Element für modale Fenster zunutze machen kann. Wir wollen die dort beschriebene Vorgehensweise dazu benutzen, um Vorlagen für Ersatzdialoge zu erstellen. Dabei verwenden wir Eingabeelemente wie <button>
und <input>
, jedoch ohne ein umgebendes Formular.
Vorlage für alert-Fenster
Das Ziel ist eine Dialog-Box mit einem OK-Button, sowie einem "Fenster schließen"-Button. Das könnte so aussehen:
<dialog id="alert" role="dialog" aria-labelledby="alert-dialog-heading">
<button class="close">Schließen</button>
<h2 id="alert-dialog-heading">Info</h2>
<p>Dieses Fenster können Sie bedenkenlos wieder schließen.</p>
<p class="button-row">
<button name="ok">OK</button>
</p>
</dialog>
Vorlage für confirm-Fenster
Wenn es nur um das Einholen einer Bestätigung geht, benötigen wir zwei Buttons (klassischerweise "OK" und "Abbrechen"):
<dialog id="confirm" role="dialog" aria-labelledby="confirm-dialog-heading">
<button class="close">Schließen</button>
<h2 id="confirm-dialog-heading">Bestätigung</h2>
<p>Alles klar?</p>
<p class="button-row">
<button name="ok">OK</button>
<button name="cancel">Abbrechen</button>
</p>
</dialog>
Vorlage für prompt-Fenster
Zusätzlich zu "OK" und "Abbrechen" benötigen wir nun auch noch eine Eingabezeile:
<dialog id="prompt" role="dialog" aria-labelledby="prompt-dialog-heading">
<button class="close">Schließen</button>
<h2 id="confirm-dialog-heading">Eingabe</h2>
<p>
<label for="prompt-data">Was möchten Sie uns sagen?</label>
<input id="prompt-data" name="data">
</p>
<p class="button-row">
<button name="ok">OK</button>
<button name="cancel">Abbrechen</button>
</p>
</dialog>
Zusammenspiel von JavaScript und Dialog-Boxen
Jetzt, da wir schicke Dialog-Boxen haben, können wir die Methoden alert, confirm und prompt nachbilden. Dazu machen wir uns das Aufteilen des Programmcodes und die Nutzung von Callback-Funktionen zunutze.
window.myAlert
Unsere Dialog-Box für Hinweise benötigt eigentlich nur, dass sie angezeigt wird. Sollten im Programmverlauf aber mehrere solche Hinweise ausgegeben werden, würde jeder neue Hinweis den alten ersetzen, bevor der Benutzer das Fenster selbst geschlossen hat. Daher ist es sinnvoll, auch bei einem einfachen Hinweisfenster die Verwendung von Callback-Funktionen anzubieten, die ermöglichen, dass keine weiteren Programmteile ausgeführt werden, solange der Benutzer das Fenster nicht weggeklickt hat:
window.myAlert = function (text, OK, cancel) {
var dialog = document.querySelector("#alert"),
textElement = document.querySelector("#alert [data-text]");
if (dialog && textElement) {
textElement.innerText = (text && text.length ? text : "");
dialog.setCallback("cancel", cancel);
dialog.setCallback("ok", OK);
dialog.show();
}
}
myAlert
nimmt drei Parameter entgegen. Der erste (text
) ist der anzuzeigende Hinweis, also ein String. Der zweite Parameter (OK
) ist für die Callback-Funktion gedacht, die ausgeführt werden soll, wenn der Benutzer den OK-Button betätigt. Der dritte Parameter (cancel
) ist für eine Funktion gedacht, die ausgeführt werden soll, wenn statt des OK-Buttons der Schließen-Button betätigt wird.Die Zuordnung der Funktionen zu den Buttons regelt unsere Erweiterung im Polyfill, die für die Fälle "ok" und "cancel" schon alles vorbereitet hat. Wir brauchen die Funktionen hier nur entsprechend weiterzureichen.
window.myConfirm
Im Grunde funktioniert myConfirm exakt gleich wie myAlert. Es verwendet nur eine andere Vorlage, die eben auch einen Abbrechen-Button hat:
window.myConfirm = function (text, OK, cancel) {
var dialog = document.querySelector("#confirm"),
textElement = document.querySelector("#confirm [data-text]");
if (dialog && textElement) {
textElement.innerText = (text && text.length ? text : "");
dialog.setCallback("cancel", cancel);
dialog.setCallback("ok", OK);
dialog.show();
}
}
window.myPrompt
Die ursprüngliche prompt-Methode ermittelt einen String, den der Benutzer eingeben soll. Daher müssen wir den Wert des Eingabefelds bei der Callback-Funktion im "ok"-Falle mitgeben. Außerdem bietet window.prompt
auch die Möglichkeit, einen Standardwert festzulegen, der schon vorbelegt wird, was wir ebenfalls unterstützen werden:
window.myPrompt = function (text, OK, cancel, defaultValue) {
var dialog = document.querySelector("#prompt"),
inputElement = document.querySelector('#prompt [name="data"]'),
textElement = document.querySelector("#prompt [data-text]");
if (dialog && textElement) {
inputElement.value = (defaultValue && defaultValue.length ? defaultValue : "");
textElement.innerText = (text && text.length ? text : "");
dialog.setCallback("cancel", cancel);
dialog.setCallback("ok", function () {
OK(inputElement.value);
});
dialog.show();
}
}
<input>
-Elements als Parameter mitgeben zu können.Der vierte Parameter enthält bei Bedarf einen Wert, der im Eingabefeld vorbelegt werden soll. Enthält er nichts Brauchbares, schreibt
myPrompt
einen Leerstring hinein, was eventuell vorhandene vorherige Werte (von vielleicht vorherigen Aufrufen) wieder entfernt.Fertiges Beispiel mit myAlert, myConfirm und myPrompt
Wenn man alle Teile zusammenfügt, kann man nun unsere neuen Möglichkeiten testen:
<section>
<h2>Alert-Fenster</h2>
<p><button data-js="alert">myAlert()</button></p>
</section>
<section>
<h2>Confirm-Fenster</h2>
<p>
<button data-js="confirm">myConfirm()</button>
Ergebnis:
<output></output>
</p>
</section>
<section>
<h2>Prompt-Fenster</h2>
<p>
<button data-js="prompt">myPrompt()</button>
Ergebnis:
<output></output>
</p>
</section>
// Demo: alle Buttons mit Funktionalität ausrüsten
Array.prototype.slice.call(
document.querySelectorAll("section")
).forEach(function(section) {
var button = section.querySelector("[data-js]"),
output = section.querySelector("output");
if (button) {
button.addEventListener("click", function () {
switch (button.dataset.js) {
case "alert":
myAlert("Dieses Fenster können Sie bedenkenlos wieder schließen.");
break;
case "confirm":
myConfirm(
"Sind Sie mit SELFHTML zufrieden?",
function () {
output.className = "ok";
output.value = "ok";
},
function () {
output.className = "false";
output.value = "false";
}
);
break;
case "prompt":
myPrompt(
"Was möchten Sie uns sagen?",
function (result) {
output.className = "ok";
output.value = '"' + result + '"';
},
function () {
output.className = "false";
output.value = "false";
}
);
break;
}
});
}
});
window
-Methode ausgelöst, die in die jeweils vorbereiteten <output>
-Elemente das Ergebnis hineinschreibt. Dabei wird berücksichtigt, ob der Rückgabewert ein boolesches false ist (wenn "Abbrechen" oder "Schließen" bedient wurde), oder ob der OK-Button bedient wurde, was bei myPrompt
zu einer Zeichenkette und bei myConfirm
zu einem true
führt.Das Anfangsbeispiel überarbeitet
In unserem früheren Beispiel verwendeten wir eine Ersatzfunktion frage
, um den folgenden Programmablauf an eine weitere Funktion zu übergeben. Diese Funktion können wir uns nun sparen, da wir mit den Callback-Funktionen das gleiche erreichen:
// die nicht-lineare Programm-Logik
function start () {
// alter erfragen
window.myPrompt(
"Wie alt sind Sie?",
// Funktion für OK-Button
weiter,
// Funktion für Abbrechen-Button oder Schließen-Button
weiter
);
}
function weiter (alter) {
if (alter < 18) {
window.myConfirm(
"Du bist also noch nicht erwachsen?",
// Funktion für OK-Button
function () {
ende(alter, true);
},
// Funktion für Abbrechen-Button oder Schließen-Button
function () {
ende(alter);
}
);
} else {
ende(alter, false);
}
}
function ende (alter, minderjaehrig) {
window.myAlert(
"Alter: " + alter + " ("
+ (minderjaehrig ? "" : "aber nicht ")
+ "minderjährig)"
);
}
// Programm starten
start();
Individuelle Lösungen
Mit window.confirm
und window.prompt
kann man nur sehr allgemeine Nutzereingaben verarbeiten: Boolescher Wert (confirm) oder Zeichenkette (prompt). Wenn wir unsere neu erarbeiteten Alternativen von oben anschauen, sehen wir, dass der ganze Aufwand nicht nur den Vorteil der freien visuellen Gestaltung bietet, sondern auch Möglichkeiten, beliebig zu erweitern.
Das folgende Beispiel verwendet eine Ergänzung im Polyfill, die darauf prüft, ob bei der Verwendung der ENTER-Taste der Fokus sich vielleicht in einem Eingabe-Element befindet, was dann natürlich nicht zum Schließen des Dialogs (mit der OK-Button-Funktion) führen darf. Desweiteren verwendet das Beispiel eine recht aufgeblasene Funktion zum Erstellen eines Dialog-Fensters, das dafür aber je nach Bedarf individuell schon beim Aufruf konfiguriert werden kann:
// neue window-Methode
window.myDialog = function (data, OK, cancel) {
var dialog = document.querySelector("#my-dialog"),
buttonRow = document.querySelector("#my-dialog .button-row"),
heading = document.querySelector("#my-dialog-heading"),
element, p, prop;
if (dialog && buttonRow) {
// Standard-Titel
if (heading) {
heading.textContent = "Eingabe";
}
// jedes <ul> und <p> entfernen, außer <p class="button-row">
Array.prototype.slice.call(
dialog.querySelectorAll("ul, p:not(.button-row)")
).forEach(function (p) {
p.parentNode.removeChild(p);
});
// Elemente erstellen und gegebenenfalls mit Inhalten befüllen
for (prop in data) {
// alles bekommt ein <p> drumherum
p = document.createElement("p");
buttonRow.parentNode.insertBefore(p, buttonRow);
// simple Textausgabe
if (data[prop].type && data[prop].type == "info") {
p.textContent = data[prop].text;
}
// anderer Titel
if (data[prop].type && data[prop].type == "title"
&& heading
) {
heading.textContent = data[prop].text;
// neues <p> wird hierfür nicht benötigt
p.parentNode.removeChild(p);
}
// numerischer Wert
if (data[prop].type && data[prop].type == "number") {
// <label> als Kindelement für Beschriftung
p.appendChild(document.createElement("label"));
p.lastChild.appendChild(
document.createTextNode(data[prop].text + " ")
);
// <input type="number">
element = p.appendChild(
document.createElement("input")
);
if (data[prop].hasOwnProperty("max")) {
element.max = data[prop]["max"];
}
if (data[prop].hasOwnProperty("min")) {
element.min = data[prop]["min"];
}
if (data[prop].hasOwnProperty("step")) {
element.step = data[prop]["step"];
}
element.name = prop;
element.type = "number";
element.value = element.min = data[prop]["min"] || 0;
if (data[prop].default) {
element.value = data[prop].default;
}
}
// Mehrfachauswahl
if (data[prop].type && data[prop].type == "multiple") {
p.textContent = data[prop].text;
// alle Optionen wandern in ein <ul>
element = document.createElement("ul");
buttonRow.parentNode.insertBefore(element, buttonRow);
data[prop].options.forEach(function (d, index) {
var input = document.createElement("input"),
label = document.createElement("label"),
li = document.createElement("li");
// <li> in <ul> einhängen
element.appendChild(li);
input.id = prop + "-" + index;
input.name = prop + "-" + index;
input.type = "checkbox";
input.value = d;
li.appendChild(input);
label.htmlFor = prop + "-" + index;
label.textContent = " " + d
li.appendChild(label);
if (data[prop].default && data[prop].default == d) {
input.setAttribute("checked", "checked");
}
});
}
// Einfachauswahl
if (data[prop].type && data[prop].type == "select") {
// <label> als Kindelement für Beschriftung
p.appendChild(document.createElement("label"));
p.lastChild.appendChild(
document.createTextNode(data[prop].text + " ")
);
// alle Optionen wandern in ein <ul>
element = p.appendChild(
document.createElement("select")
);
element.name = prop;
data[prop].options.forEach(function (d) {
var o = document.createElement("option");
o.textContent = d;
o.value = d;
element.appendChild(o);
if (data[prop].default && data[prop].default == d) {
o.setAttribute("selected", "selected");
}
});
}
// Texteingabe
if (data[prop].type && data[prop].type == "text") {
// <label> als Kindelement für Beschriftung
p.appendChild(document.createElement("label"));
p.lastChild.appendChild(
document.createTextNode(data[prop].text)
);
// alle Optionen wandern in ein <ul>
element = p.appendChild(
document.createElement("textarea")
);
element.name = prop;
if (data[prop].default) {
element.textContent = data[prop].default;
}
}
}
dialog.setCallback("cancel", cancel);
dialog.setCallback("ok", function () {
var result = {},
elements;
// Ergebnisse ermitteln
for (prop in data) {
elements = Array.prototype.slice.call(
dialog.querySelectorAll('[name^="' + prop + '"]')
);
if (data[prop].type && data[prop].type == "multiple") {
result[prop] = [];
elements.forEach(function (element) {
if (element.checked) {
result[prop].push(element.value);
}
});
} else {
if (data[prop].type != "title"
&& data[prop].type != "info"
) {
result[prop] = null;
if (elements[0]) {
result[prop] = elements[0].value;
}
}
}
}
// Ergebnisse an die Callback-Funktion zurück geben
OK(result);
});
dialog.show();
}
}
for in
-Schleife werden die in dem Funktionsparameter data
enthaltenen Eingabedaten abgearbeitet. Dabei handelt es sich um einfache Objekte, die eine Eigenschaft text
enthalten, welcher als eigentlicher Wert verstanden wird. Zusätzlich müssen sie eine Eigenschaft type
enthalten, wonach dann die passenden Eingabelemente erzeugt werden. Um den Fenstertitel zu ändern gibt es extra den Typ title
. Für eine reine Textzeile ohne weitere Eingabemöglichkeit gibt es den Typ info
.Da für jedes Eingabedatum eine neue Zeile benötigt wird, wird grundsätzlich ein neuer Textabsatz eingefügt, der vor dem Textabsatz mit den Buttons OK
und Abbrechen
zu liegen kommt.
<p>
- und <ul>
-Elemente, welche bei der Vorbereitung des Dialogfensters natürlich erst entfernt werden.// Aufruf mit selbst konfigurierten Eingabemöglichkeiten
myDialog(
// data
{
instructions: {
text: "Bitte seien Sie jetzt komplett ehrlich und füllen Sie wahrheitsgemäß alles aus!",
type: "info"
},
title: {
text: "Sonderabfrage",
type: "title"
},
sex: {
"default": "weiblich",
options: ["männlich", "weiblich", "divers"],
text: "Geschlecht",
type: "select"
},
age: {
"default": 18,
"max": 150,
"min": 0,
step: 1,
text: "Alter",
type: "number"
},
preferences: {
"default": "Pop",
options: ["Jazz", "Swing", "Latin", "Klassik", "Hiphop", "Pop"],
text: "Diese Musik mag ich gerne",
type: "multiple"
},
message: {
text: "Das will ich mitteilen",
type: "text"
}
},
// OK
function (data) {
var output = document.querySelector("main pre"),
prop,
result = "Ergebnis:\r\n=========\r\n\r\n";
for (prop in data) {
result += prop + ":";
if (typeof data[prop] == "object") {
data[prop].forEach(function (value, index) {
result += (index ? "," : "") + "\r\n\t" + value;
});
} else {
result += " " + data[prop];
}
result += "\r\n";
}
if (output) {
output.textContent = result;
}
},
// cancel
function () {
var output = document.querySelector("main pre");
if (output) {
output.textContent = "(kein Ergebnis)";
}
}
);
default
genannte Eigenschaft in den Unterobjekten genutzt werden kann.Der Phantasie des Programmierers sind so keine Grenzen gesetzt. Auch ist die hier beschriebene Vorgehensweise nur eine von unendlich vielen denkbaren. Sie soll zeigen, dass man sich auf diese Art ein kleines Rahmenwerk (Framework) schaffen kann, das dann ziemlich flexibel beliebige Dialog-Boxen erzeugen und die darin eingetragenen Informationen wieder sinnvoll zurück liefern kann.
Die Sache mit dem Fokus
Unsere Beispiele haben keinen Mechanismus, der dafür sorgt, dass der Anwender den Fokus aus einem geöffneten Dialog nicht heraus bewegen kann, um die Bedienbarkeit rein mit der Tastatur unter allen Umständen zu gewährleisten. In diesem Tutorial beschränken wir uns lediglich darauf, den Fokus auf den Schließen-Button in der Dialog-Box zu setzen. Andere Lösungen, die wesentlich aufwändiger gearbeitet sind, treiben in dieser Hinsicht deutlich mehr Aufwand.
Es sei auch nicht verschwiegen, dass die Frage "wohin mit dem Fokus danach?" keine triviale Frage ist. Je nach Umständen möchte man den Fokus wieder dort haben, wo er vor der Anzeige der Dialog-Box war. Dann muss man dafür Sorge tragen, dass nach dem Schließen des Dialogs der Fokus auch wieder dort hin zurück kehrt. Aber in anderen Fällen möchte man das vielleicht gerade eben nicht. Es hängt also von der Gestaltung der Applikation ab, wie Benutzer hinsichtlich des Eingabefokus angeleitet werden sollen.
Siehe auch
Weblinks
- github.com: The Incredible Accessible Modal Window
- github.com: A11y Dialog
window
-Methode direkt genutzt. Die Variablealter
enthält das eingegebene Alter (oder was auch immer der Benutzer gerade eingegeben hat!) und die Variableminderjaehrig
entweder den Wertfalse
odertrue
.