mirror of
https://github.com/webmin/webmin.git
synced 2026-10-29 22:50:58 +00:00
713 lines
27 KiB
JavaScript
713 lines
27 KiB
JavaScript
/*
|
|
* ui-lib.js
|
|
* Behavior for the widgets added to ui-lib.pl.
|
|
*
|
|
* Everything is wired through delegated event listeners keyed off
|
|
* data-ui-* attributes, so no inline handlers are generated and pages
|
|
* remain compatible with a strict Content-Security-Policy. Tabs, sorting
|
|
* and select-all links keep using the scripts the existing ui-lib
|
|
* functions and themes already provide.
|
|
*
|
|
* Besides the filters, confirmations, choice lists and selection lists,
|
|
* this script drives the drop-down menus of ui_dropdown (one open at a
|
|
* time, closed by a click elsewhere or Escape, arrow keys between the
|
|
* entries, flipped to stay in the window) and the dialogs of ui_dialog
|
|
* (opened by data-ui-dialog controls, filled from data-ui-dialog-values,
|
|
* closed by data-ui-dialog-close controls or a click on the backdrop).
|
|
* window.webminUiWidgets.openDialog(id, values) and closeDialog(id) do
|
|
* the same from module scripts, and webminUiWidgets.confirm(message,
|
|
* options) asks a question in the confirmation dialog that
|
|
* data-ui-confirm controls use, resolving with the answer.
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
// Replace old listeners when Ajax navigation reloads this script.
|
|
if (window.webminUiWidgets) window.webminUiWidgets.remove();
|
|
var listeners = [];
|
|
var dropdownObserver;
|
|
function on(type, fn, capture, target) {
|
|
target = target || document;
|
|
target.addEventListener(type, fn, capture);
|
|
listeners.push([ type, fn, capture, target ]);
|
|
}
|
|
window.webminUiWidgets = {
|
|
remove: function () {
|
|
if (dropdownObserver) dropdownObserver.disconnect();
|
|
listeners.forEach(function (l) {
|
|
l[3].removeEventListener(l[0], l[1], l[2]);
|
|
});
|
|
}
|
|
};
|
|
|
|
// Filter table rows or list entries in the target container by the
|
|
// text typed into a ui_search box, leaving header rows in place
|
|
function applyFilter(input) {
|
|
var sel = input.getAttribute('data-ui-filter');
|
|
var target = sel && document.querySelector(sel);
|
|
if (!target) return;
|
|
var query = input.value.trim().toLowerCase();
|
|
var items = target.querySelectorAll(
|
|
'tbody tr, .ui_list_item, [data-ui-filter-item]');
|
|
items.forEach(function (item) {
|
|
if (item.classList.contains('ui_columns_heads') ||
|
|
item.classList.contains('ui_columns_heading')) return;
|
|
item.hidden = query !== '' &&
|
|
item.textContent.toLowerCase().indexOf(query) < 0;
|
|
});
|
|
}
|
|
|
|
// Confirmation prompts on buttons and links carrying data-ui-confirm,
|
|
// including existing ui_submit buttons given the attribute in tags.
|
|
// The click is caught before a theme or inline handler performs the
|
|
// action, the question is asked in a dialog, and the click is replayed
|
|
// once confirmed. data-ui-confirm-title, data-ui-confirm-ok and
|
|
// data-ui-confirm-state refine the dialog; a danger action button asks
|
|
// in the danger state by default.
|
|
var replaying = null;
|
|
on('click', function (e) {
|
|
// Disabled action links must not run handlers or open confirmations.
|
|
var disabled = e.target.closest && e.target.closest('.ui_action[aria-disabled="true"]');
|
|
if (disabled) {
|
|
e.preventDefault();
|
|
e.stopImmediatePropagation();
|
|
return;
|
|
}
|
|
var confirmer = e.target.closest && e.target.closest('[data-ui-confirm]');
|
|
if (!confirmer || confirmer === replaying) return;
|
|
e.preventDefault();
|
|
e.stopImmediatePropagation();
|
|
openDropdowns().forEach(function (menu) { closeDropdown(menu); });
|
|
var state = confirmer.getAttribute('data-ui-confirm-state') ||
|
|
(confirmer.classList.contains('ui_action_danger') ? 'danger' : 'warning');
|
|
uiConfirm(confirmer.getAttribute('data-ui-confirm'), {
|
|
title: confirmer.getAttribute('data-ui-confirm-title'),
|
|
ok: confirmer.getAttribute('data-ui-confirm-ok'),
|
|
state: state
|
|
}).then(function (yes) {
|
|
if (!yes) return;
|
|
replaying = confirmer;
|
|
try { confirmer.click(); } finally { replaying = null; }
|
|
});
|
|
}, true);
|
|
|
|
on('input', function (e) {
|
|
var input = e.target.closest && e.target.closest('[data-ui-filter]');
|
|
if (input) applyFilter(input);
|
|
});
|
|
// Choice lists : focusing an input of an option selects that option,
|
|
// as the existing ui_opt_textbox does
|
|
on('focusin', function (e) {
|
|
// Links, help buttons and other focusable content do not change the
|
|
// selected option; only focusing one of its editable controls does.
|
|
if (!e.target.matches ||
|
|
!e.target.matches('input:not([type="button"]):not([type="submit"]):not([type="reset"]), select, textarea')) return;
|
|
var item = e.target.closest && e.target.closest('.ui_choice_item');
|
|
if (!item) return;
|
|
var input = item.querySelector('input[type="radio"]');
|
|
if (input && e.target !== input && !input.checked && !input.disabled) {
|
|
input.checked = true;
|
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
});
|
|
|
|
// Select switches : show the block of the chosen option, hide the rest
|
|
function applySwitch(select) {
|
|
var box = select.closest('.ui_select_switch');
|
|
if (!box) return;
|
|
box.querySelectorAll('.ui_select_switch_panel').forEach(function (panel) {
|
|
// Nested switches manage their own panels independently.
|
|
if (panel.closest('.ui_select_switch') !== box) return;
|
|
panel.hidden =
|
|
panel.getAttribute('data-ui-switch-value') !== select.value;
|
|
});
|
|
}
|
|
on('change', function (e) {
|
|
var select = e.target;
|
|
if (select.matches && select.matches('select[data-ui-switch]')) {
|
|
applySwitch(select);
|
|
}
|
|
});
|
|
|
|
// Native reset restores select values after the reset event, without
|
|
// firing change. Update the panels once those values have been restored.
|
|
on('reset', function (e) {
|
|
window.setTimeout(function () {
|
|
if (e.defaultPrevented) return;
|
|
document.querySelectorAll('select[data-ui-switch]').forEach(function (select) {
|
|
if (select.form === e.target) applySwitch(select);
|
|
});
|
|
}, 0);
|
|
});
|
|
|
|
// Multiple selection lists submit newline-joined values in a hidden input.
|
|
var multiAnchors = new WeakMap();
|
|
// Find the checkbox inside any theme wrapper.
|
|
function multiInput(item) {
|
|
return item.querySelector('input[type="checkbox"]');
|
|
}
|
|
// Closing clears the query and returns focus to the button.
|
|
function openMultiFilter(box, open) {
|
|
var filter = box.querySelector('.ui_multi_filter');
|
|
var search = box.querySelector('[data-ui-multi-search]');
|
|
if (!filter || !search || search.disabled) return;
|
|
filter.classList.toggle('ui_multi_filter_open', open);
|
|
if (!open) search.value = '';
|
|
applyMulti(box);
|
|
(open ? search : filter.querySelector('.ui_multi_filter_toggle')).focus();
|
|
}
|
|
function applyMulti(box, reset) {
|
|
// Resets and history restores need not fire change events.
|
|
var mode = box.querySelector('.ui_multi_modes select, .ui_multi_modes input[type="radio"]:checked');
|
|
var body = box.querySelector('.ui_multi_body');
|
|
var hide = JSON.parse(box.getAttribute('data-ui-multi-hide') || '[]');
|
|
if (body) body.hidden = !!mode && hide.indexOf(mode.value) >= 0;
|
|
var search = box.querySelector('[data-ui-multi-search]');
|
|
var query = search ? search.value.trim().toLowerCase() : '';
|
|
// Reopen restored searches.
|
|
var filter = box.querySelector('.ui_multi_filter');
|
|
if (filter) {
|
|
if (query) filter.classList.add('ui_multi_filter_open');
|
|
filter.classList.toggle('ui_multi_filter_active', query !== '');
|
|
filter.querySelector('.ui_multi_filter_toggle').setAttribute('aria-expanded',
|
|
String(filter.classList.contains('ui_multi_filter_open')));
|
|
}
|
|
var fold = box.querySelector('[data-ui-multi-action="children"]');
|
|
var folded = !!(fold && fold.checked);
|
|
var matched = 0;
|
|
var shownGroups = new Set();
|
|
var chosen = [];
|
|
var selectedCount = 0;
|
|
box.querySelectorAll('.ui_multi_item').forEach(function (item) {
|
|
var input = multiInput(item);
|
|
if (input && input.checked) chosen.push(input.value);
|
|
// Show child counts only while folded.
|
|
var note = item.querySelector('.ui_multi_note');
|
|
if (note) note.hidden = !folded;
|
|
// The parent covers folded children.
|
|
if (folded && item.getAttribute('data-ui-multi-level')) {
|
|
item.hidden = true;
|
|
return;
|
|
}
|
|
// Retain folded selections, but count only unfolded entries.
|
|
if (input && input.checked) selectedCount++;
|
|
var match = query === '' ||
|
|
(item.getAttribute('data-ui-multi-text') || '').toLowerCase().indexOf(query) >= 0;
|
|
if (match) {
|
|
matched++;
|
|
shownGroups.add(item.getAttribute('data-ui-multi-group'));
|
|
}
|
|
item.hidden = !match;
|
|
});
|
|
// Hide headings whose entries are all filtered or folded.
|
|
box.querySelectorAll('[data-ui-multi-heading]').forEach(function (heading) {
|
|
heading.hidden = !shownGroups.has(heading.getAttribute('data-ui-multi-heading'));
|
|
});
|
|
var empty = box.querySelector('.ui_multi_empty');
|
|
if (empty) empty.hidden = matched > 0;
|
|
// Hide the count for empty selections or hidden lists.
|
|
var count = box.querySelector('.ui_multi_count');
|
|
if (count) {
|
|
count.textContent = box.getAttribute('data-ui-multi-text-selected')
|
|
.replace('$1', selectedCount);
|
|
count.hidden = selectedCount === 0 || !!(body && body.hidden);
|
|
}
|
|
var name = box.getAttribute('data-ui-multi');
|
|
box.querySelectorAll('input[type="hidden"]').forEach(function (hidden) {
|
|
if (hidden.name !== name) return;
|
|
var order = box.getAttribute('data-ui-multi-order');
|
|
if (order !== null) {
|
|
// Capture browser-decoded values once, surviving script reinjection.
|
|
if (order === '') {
|
|
order = JSON.stringify(hidden.value.split('\n'));
|
|
box.setAttribute('data-ui-multi-order', order);
|
|
}
|
|
// Retain legacy selection order; newly added entries go first.
|
|
var checked = new Set(chosen);
|
|
var previous = (reset ? JSON.parse(order) : hidden.value.split('\n'))
|
|
.filter(function (value) { return checked.has(value); });
|
|
var retained = new Set(previous);
|
|
var added = chosen.filter(function (value) { return !retained.has(value); });
|
|
hidden.value = added.reverse().concat(previous).join('\n');
|
|
} else {
|
|
hidden.value = chosen.join('\n');
|
|
}
|
|
});
|
|
}
|
|
// Bulk actions affect only visible, enabled rows.
|
|
function eachShownRow(box, fn) {
|
|
box.querySelectorAll('.ui_multi_item').forEach(function (item) {
|
|
var input = multiInput(item);
|
|
if (input && !input.disabled && !item.hidden) fn(input);
|
|
});
|
|
}
|
|
// Prevent text selection during Shift-click.
|
|
on('mousedown', function (e) {
|
|
var item = e.target.closest && e.target.closest('.ui_multi_item');
|
|
if (e.shiftKey && e.button === 0 && item &&
|
|
item.closest('[data-ui-multi]') &&
|
|
!e.target.closest('input, a, button')) e.preventDefault();
|
|
});
|
|
on('input', function (e) {
|
|
var search = e.target.closest && e.target.closest('[data-ui-multi-search]');
|
|
var box = search && search.closest('[data-ui-multi]');
|
|
if (box) applyMulti(box);
|
|
});
|
|
// Capture Enter/Escape before theme handlers can submit or leave the page.
|
|
on('keydown', function (e) {
|
|
var search = e.target.closest && e.target.closest('[data-ui-multi-search]');
|
|
var box = search && search.closest('[data-ui-multi]');
|
|
if (!box || e.isComposing) return;
|
|
if (e.key === 'Escape' || e.key === 'Enter') {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
if (e.key === 'Escape') openMultiFilter(box, false);
|
|
}, true);
|
|
on('change', function (e) {
|
|
var box = e.target.closest && e.target.closest('[data-ui-multi]');
|
|
if (!box) return;
|
|
var t = e.target;
|
|
if (t.matches('.ui_multi_modes select, .ui_multi_modes input[type="radio"]')) {
|
|
// Start a new range after a mode change.
|
|
multiAnchors.delete(box);
|
|
}
|
|
applyMulti(box);
|
|
});
|
|
on('click', function (e) {
|
|
var t = e.target.closest && e.target.closest(
|
|
'[data-ui-multi-action], .ui_multi_item');
|
|
var box = t && t.closest('[data-ui-multi]');
|
|
if (!box) return;
|
|
var action = t.getAttribute('data-ui-multi-action');
|
|
if (action === 'filter' || action === 'filter-clear') {
|
|
// Cross clears text or closes an empty filter; funnel toggles.
|
|
e.preventDefault();
|
|
if (t.disabled) return;
|
|
var search = box.querySelector('[data-ui-multi-search]');
|
|
var open = action === 'filter-clear' ? search.value !== '' :
|
|
!t.closest('.ui_multi_filter').classList.contains('ui_multi_filter_open');
|
|
if (action === 'filter-clear') search.value = '';
|
|
openMultiFilter(box, open);
|
|
return;
|
|
}
|
|
if (action === 'all' || action === 'invert') {
|
|
// Apply the bulk action without following the link.
|
|
e.preventDefault();
|
|
eachShownRow(box, function (input) {
|
|
input.checked = action === 'all' || !input.checked;
|
|
});
|
|
multiAnchors.delete(box);
|
|
}
|
|
else if (action) {
|
|
// The switch is handled by change.
|
|
return;
|
|
}
|
|
else {
|
|
var input = multiInput(t);
|
|
if (!input || input.disabled || t.hidden) return;
|
|
// Only row backgrounds need toggling; checkboxes and labels do it natively.
|
|
if (e.target !== input) {
|
|
if (e.target.closest('input, label, a, button')) return;
|
|
input.checked = !input.checked;
|
|
}
|
|
// Apply the clicked state across visible, enabled rows in either direction.
|
|
if (e.shiftKey) {
|
|
var shown = [];
|
|
eachShownRow(box, function (row) { shown.push(row); });
|
|
var start = shown.indexOf(multiAnchors.get(box));
|
|
var end = shown.indexOf(input);
|
|
if (start >= 0 && end >= 0) {
|
|
var checked = input.checked;
|
|
shown.slice(Math.min(start, end), Math.max(start, end) + 1)
|
|
.forEach(function (row) { row.checked = checked; });
|
|
}
|
|
}
|
|
multiAnchors.set(box, input);
|
|
}
|
|
applyMulti(box);
|
|
});
|
|
// Wait for native reset before syncing rows and submitted values.
|
|
on('reset', function (e) {
|
|
window.setTimeout(function () {
|
|
if (e.defaultPrevented) return;
|
|
document.querySelectorAll('[data-ui-multi]').forEach(function (box) {
|
|
if (e.target.contains(box)) {
|
|
multiAnchors.delete(box);
|
|
applyMulti(box, true);
|
|
}
|
|
});
|
|
}, 0);
|
|
});
|
|
// Sync new or restored widgets and discard old range anchors.
|
|
function initMulti() {
|
|
document.querySelectorAll('[data-ui-multi]').forEach(function (box) {
|
|
multiAnchors.delete(box);
|
|
applyMulti(box);
|
|
});
|
|
}
|
|
// Back/forward cache restores do not reload scripts.
|
|
on('pageshow', initMulti, false, window);
|
|
if (document.readyState === 'loading') {
|
|
on('DOMContentLoaded', initMulti);
|
|
} else {
|
|
initMulti();
|
|
}
|
|
|
|
// ---- Drop-down menus ----
|
|
// The details element opens and closes natively; this adds the
|
|
// behavior a menu needs on top of that.
|
|
function openDropdowns() {
|
|
return document.querySelectorAll('details.ui_dropdown[open]');
|
|
}
|
|
function dropdownItems(menu) {
|
|
return Array.prototype.slice.call(menu.querySelectorAll(
|
|
'.ui_dropdown_item:not(:disabled):not([aria-disabled="true"])'));
|
|
}
|
|
function closeDropdown(menu, refocus) {
|
|
menu.removeAttribute('open');
|
|
menu.classList.remove('ui_dropdown_flip');
|
|
var summary = menu.querySelector(':scope > summary');
|
|
if (refocus && summary) summary.focus();
|
|
}
|
|
// positionDropdown(menu) keeps one menu open and flips it to fit the window.
|
|
function positionDropdown(menu) {
|
|
if (!menu.matches || !menu.matches('details.ui_dropdown')) return;
|
|
if (!menu.open) {
|
|
menu.classList.remove('ui_dropdown_flip');
|
|
return;
|
|
}
|
|
openDropdowns().forEach(function (other) {
|
|
if (other !== menu) closeDropdown(other);
|
|
});
|
|
var panel = menu.querySelector('.ui_dropdown_menu');
|
|
if (!panel) return;
|
|
menu.classList.remove('ui_dropdown_flip');
|
|
var rect = panel.getBoundingClientRect();
|
|
var width = document.documentElement.clientWidth;
|
|
var end = menu.classList.contains('ui_dropdown_end');
|
|
if ((!end && rect.right > width - 8) || (end && rect.left < 8)) {
|
|
menu.classList.add('ui_dropdown_flip');
|
|
}
|
|
}
|
|
// Attribute observers run before painting; native toggle events may arrive
|
|
// after the panel has already appeared on the wrong side.
|
|
dropdownObserver = new MutationObserver(function (records) {
|
|
var menus = new Set();
|
|
records.forEach(function (record) {
|
|
if (record.target.matches('details.ui_dropdown')) menus.add(record.target);
|
|
});
|
|
menus.forEach(function (menu) {
|
|
if (menu.isConnected) positionDropdown(menu);
|
|
});
|
|
});
|
|
dropdownObserver.observe(document, {
|
|
attributes: true, attributeFilter: ['open'], subtree: true
|
|
});
|
|
// Also place menus that were already open when this script loaded, or
|
|
// were inserted as open markup. Toggle does not bubble.
|
|
openDropdowns().forEach(positionDropdown);
|
|
on('toggle', function (e) { positionDropdown(e.target); }, true);
|
|
// A click outside an open menu closes it; a chosen entry closes its
|
|
// menu once its own action has run
|
|
on('click', function (e) {
|
|
var inside = e.target.closest && e.target.closest('details.ui_dropdown[open]');
|
|
openDropdowns().forEach(function (menu) {
|
|
if (menu !== inside) closeDropdown(menu);
|
|
});
|
|
var item = e.target.closest && e.target.closest('.ui_dropdown_item');
|
|
if (item && inside && !e.defaultPrevented) {
|
|
window.setTimeout(function () { closeDropdown(inside); }, 0);
|
|
}
|
|
});
|
|
// Keep the focus, and so the menu, while clicking on its padding,
|
|
// headings and separators
|
|
on('mousedown', function (e) {
|
|
var panel = e.target.closest && e.target.closest('.ui_dropdown_menu');
|
|
if (panel && !e.target.closest('input, select, textarea, a, button, [tabindex]')) {
|
|
e.preventDefault();
|
|
}
|
|
});
|
|
on('keydown', function (e) {
|
|
var menu = e.target.closest && e.target.closest('details.ui_dropdown');
|
|
if (!menu || e.isComposing) return;
|
|
var items = dropdownItems(menu);
|
|
var current = items.indexOf(document.activeElement);
|
|
if (e.key === 'Escape') {
|
|
if (!menu.open) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
closeDropdown(menu, true);
|
|
}
|
|
else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
if (!menu.open) {
|
|
menu.setAttribute('open', '');
|
|
// Place the panel before focusing an item can scroll it into view.
|
|
positionDropdown(menu);
|
|
}
|
|
if (!items.length) return;
|
|
var next;
|
|
if (e.key === 'ArrowDown') {
|
|
next = current < 0 ? 0 : (current + 1) % items.length;
|
|
} else {
|
|
next = current < 0 ? items.length - 1 :
|
|
(current - 1 + items.length) % items.length;
|
|
}
|
|
items[next].focus();
|
|
}
|
|
else if ((e.key === 'Home' || e.key === 'End') && menu.open && items.length) {
|
|
e.preventDefault();
|
|
items[e.key === 'Home' ? 0 : items.length - 1].focus();
|
|
}
|
|
});
|
|
// Tabbing out of an open menu closes it
|
|
on('focusout', function (e) {
|
|
var menu = e.target.closest && e.target.closest('details.ui_dropdown[open]');
|
|
if (menu && (!e.relatedTarget || !menu.contains(e.relatedTarget))) {
|
|
closeDropdown(menu);
|
|
}
|
|
});
|
|
|
|
// ---- Dialogs ----
|
|
function findDialog(ref) {
|
|
if (!ref) return null;
|
|
return typeof ref === 'string' ? document.getElementById(ref) : ref;
|
|
}
|
|
// Fill the dialog's fields from a name to value map
|
|
function fillDialog(dialog, values) {
|
|
if (!values || typeof values !== 'object') return;
|
|
Object.keys(values).forEach(function (name) {
|
|
var value = values[name];
|
|
var selector = '[name="' + name.replace(/["\\]/g, '\\$&') + '"]';
|
|
dialog.querySelectorAll(selector).forEach(function (field) {
|
|
if (field.type === 'checkbox' || field.type === 'radio') {
|
|
field.checked = Array.isArray(value) ?
|
|
value.indexOf(field.value) >= 0 :
|
|
value === true || String(field.value) === String(value);
|
|
}
|
|
else if (field.tagName === 'SELECT' && field.multiple && Array.isArray(value)) {
|
|
Array.prototype.forEach.call(field.options, function (option) {
|
|
option.selected = value.indexOf(option.value) >= 0;
|
|
});
|
|
}
|
|
else if (field.type !== 'file') {
|
|
field.value = value === null || value === undefined ? '' : value;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
// Open a dialog modally, after filling it and letting module scripts
|
|
// prepare or veto it through the ui-dialog-open event
|
|
function openDialog(ref, values, opener) {
|
|
var dialog = findDialog(ref);
|
|
if (!dialog) return null;
|
|
fillDialog(dialog, values);
|
|
var go = dialog.dispatchEvent(new CustomEvent('ui-dialog-open', {
|
|
bubbles: true, cancelable: true,
|
|
detail: { opener: opener || null, values: values || null }
|
|
}));
|
|
if (!go) return null;
|
|
if (!dialog.open) {
|
|
if (typeof dialog.showModal === 'function') dialog.showModal();
|
|
else dialog.setAttribute('open', '');
|
|
}
|
|
var focus = dialog.querySelector('[autofocus]') || dialog.querySelector(
|
|
'input:not([type="hidden"]):not([disabled]), select:not([disabled]), ' +
|
|
'textarea:not([disabled]), button:not([disabled]):not(.ui_dialog_x)');
|
|
if (focus) focus.focus();
|
|
return dialog;
|
|
}
|
|
function closeDialog(ref) {
|
|
var dialog = findDialog(ref);
|
|
if (dialog && dialog.open) dialog.close();
|
|
}
|
|
|
|
// ---- Confirmations ----
|
|
// One dialog, built on first use and reused, asks the questions of
|
|
// data-ui-confirm controls and of webminUiWidgets.confirm(). Its texts
|
|
// come from the data attributes ui_page_assets puts on this script
|
|
// tag, with English fallbacks for pages that load the script by hand.
|
|
var confirmTexts = (function () {
|
|
var me = document.currentScript || document.querySelector('script[data-ui-lib]');
|
|
var d = me ? me.dataset : {};
|
|
return { title: d.uiTextConfirm || 'Please confirm', ok: d.uiTextOk || 'OK',
|
|
cancel: d.uiTextCancel || 'Cancel', close: d.uiTextClose || 'Close' };
|
|
})();
|
|
// The state icons of ui_svg_icon, drawn the same way
|
|
var confirmIcons = {
|
|
warning: 'M8 2.6l6.4 10.4H1.6L8 2.6z|M8 6.8v2.7|M8 11.3h.01',
|
|
danger: 'M8 14.25A6.25 6.25 0 1 0 8 1.75a6.25 6.25 0 0 0 0 12.5z|M6 6l4 4|M10 6l-4 4',
|
|
info: 'M8 14.25A6.25 6.25 0 1 0 8 1.75a6.25 6.25 0 0 0 0 12.5z|M8 7.4v3.4|M8 5.1h.01',
|
|
success: 'M8 14.25A6.25 6.25 0 1 0 8 1.75a6.25 6.25 0 0 0 0 12.5z|M5.4 8.3l1.8 1.9 3.4-4',
|
|
x: 'M4.5 4.5l7 7|M11.5 4.5l-7 7'
|
|
};
|
|
function confirmIcon(name, size) {
|
|
var ns = 'http://www.w3.org/2000/svg';
|
|
var svg = document.createElementNS(ns, 'svg');
|
|
svg.setAttribute('class', 'ui_svg_icon ui_svg_icon_' + name);
|
|
svg.setAttribute('width', size);
|
|
svg.setAttribute('height', size);
|
|
svg.setAttribute('viewBox', '0 0 16 16');
|
|
svg.setAttribute('fill', 'none');
|
|
svg.setAttribute('stroke', 'currentColor');
|
|
svg.setAttribute('stroke-width', '1.5');
|
|
svg.setAttribute('stroke-linecap', 'round');
|
|
svg.setAttribute('stroke-linejoin', 'round');
|
|
svg.setAttribute('aria-hidden', 'true');
|
|
confirmIcons[name].split('|').forEach(function (d) {
|
|
var path = document.createElementNS(ns, 'path');
|
|
path.setAttribute('d', d);
|
|
svg.appendChild(path);
|
|
});
|
|
return svg;
|
|
}
|
|
function actionButton(className, attrs) {
|
|
var button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.className = className;
|
|
Object.keys(attrs).forEach(function (name) { button.setAttribute(name, attrs[name]); });
|
|
var content = document.createElement('span');
|
|
content.className = 'ui_action_content';
|
|
content.appendChild(document.createElement('span'));
|
|
button.appendChild(content);
|
|
return button;
|
|
}
|
|
// A page may supply its own dialog with the id ui_confirm, holding a
|
|
// data-ui-confirm-text element and a data-ui-confirm-ok button
|
|
function confirmDialog() {
|
|
var dialog = document.getElementById('ui_confirm');
|
|
if (dialog) return dialog;
|
|
dialog = document.createElement('dialog');
|
|
dialog.id = 'ui_confirm';
|
|
dialog.className = 'ui_dialog ui_dialog_small';
|
|
dialog.setAttribute('aria-labelledby', 'ui_confirm_title');
|
|
var head = document.createElement('header');
|
|
head.className = 'ui_dialog_head';
|
|
var titles = document.createElement('div');
|
|
titles.className = 'ui_dialog_titles';
|
|
var title = document.createElement('h2');
|
|
title.className = 'ui_dialog_title';
|
|
title.id = 'ui_confirm_title';
|
|
titles.appendChild(title);
|
|
head.appendChild(titles);
|
|
var x = document.createElement('button');
|
|
x.type = 'button';
|
|
x.className = 'ui_dialog_x';
|
|
x.setAttribute('data-ui-dialog-close', '1');
|
|
x.setAttribute('aria-label', confirmTexts.close);
|
|
x.title = confirmTexts.close;
|
|
x.appendChild(confirmIcon('x', 16));
|
|
head.appendChild(x);
|
|
dialog.appendChild(head);
|
|
var body = document.createElement('div');
|
|
body.className = 'ui_dialog_body';
|
|
var text = document.createElement('p');
|
|
text.className = 'ui_dialog_text';
|
|
text.setAttribute('data-ui-confirm-text', '1');
|
|
body.appendChild(text);
|
|
dialog.appendChild(body);
|
|
var foot = document.createElement('footer');
|
|
foot.className = 'ui_dialog_foot';
|
|
foot.appendChild(actionButton('ui_action ui_action_warning ui_action_normal ui_action_square',
|
|
{ 'data-ui-confirm-ok': '1' }));
|
|
var cancel = actionButton('ui_action ui_action_neutral ui_action_normal ui_action_square',
|
|
{ 'data-ui-dialog-close': '1', 'autofocus': '' });
|
|
cancel.querySelector('.ui_action_content > span').textContent = confirmTexts.cancel;
|
|
foot.appendChild(cancel);
|
|
dialog.appendChild(foot);
|
|
document.body.appendChild(dialog);
|
|
return dialog;
|
|
}
|
|
// Ask a question and resolve with true on OK, false otherwise. The
|
|
// options are title, ok (the label of the confirming button) and state.
|
|
// Without dialog support the browser's own prompt is used.
|
|
function uiConfirm(message, opts) {
|
|
opts = opts || {};
|
|
return new Promise(function (resolve) {
|
|
if (typeof HTMLDialogElement === 'undefined') {
|
|
resolve(window.confirm(message));
|
|
return;
|
|
}
|
|
var state = /^(?:warning|danger|info|success|neutral)$/.test(opts.state || '') ?
|
|
opts.state : 'warning';
|
|
var dialog = confirmDialog();
|
|
dialog.classList.remove('ui_dialog_warning', 'ui_dialog_danger',
|
|
'ui_dialog_info', 'ui_dialog_success', 'ui_dialog_neutral');
|
|
dialog.classList.add('ui_dialog_' + state);
|
|
var title = dialog.querySelector('.ui_dialog_title');
|
|
if (title) {
|
|
title.textContent = '';
|
|
if (confirmIcons[state]) title.appendChild(confirmIcon(state, 18));
|
|
var span = document.createElement('span');
|
|
span.textContent = opts.title || confirmTexts.title;
|
|
title.appendChild(span);
|
|
}
|
|
var text = dialog.querySelector('[data-ui-confirm-text]');
|
|
if (text) text.textContent = message;
|
|
var ok = dialog.querySelector('[data-ui-confirm-ok]');
|
|
if (ok) {
|
|
ok.className = 'ui_action ui_action_' + state + ' ui_action_normal ui_action_square';
|
|
var label = ok.querySelector('.ui_action_content > span');
|
|
if (label) label.textContent = opts.ok || confirmTexts.ok;
|
|
}
|
|
var done = false;
|
|
function finish(result) {
|
|
if (done) return;
|
|
done = true;
|
|
dialog.removeEventListener('close', onClose);
|
|
if (ok) ok.removeEventListener('click', onOk);
|
|
resolve(result);
|
|
}
|
|
function onOk() {
|
|
finish(true);
|
|
dialog.close();
|
|
}
|
|
function onClose() { finish(false); }
|
|
if (ok) ok.addEventListener('click', onOk);
|
|
dialog.addEventListener('close', onClose);
|
|
if (!openDialog(dialog)) finish(false);
|
|
});
|
|
}
|
|
window.webminUiWidgets.openDialog = openDialog;
|
|
window.webminUiWidgets.closeDialog = closeDialog;
|
|
window.webminUiWidgets.confirm = uiConfirm;
|
|
on('click', function (e) {
|
|
if (!e.target.closest) return;
|
|
var closer = e.target.closest('[data-ui-dialog-close]');
|
|
if (closer) {
|
|
var owner = closer.closest('dialog');
|
|
if (owner) {
|
|
e.preventDefault();
|
|
owner.close();
|
|
}
|
|
return;
|
|
}
|
|
var opener = e.target.closest('[data-ui-dialog]');
|
|
if (opener) {
|
|
// A link opener must not navigate
|
|
if (opener.matches('a')) e.preventDefault();
|
|
if (opener.matches(':disabled, [aria-disabled="true"]')) return;
|
|
var values = null;
|
|
try {
|
|
values = JSON.parse(opener.getAttribute('data-ui-dialog-values') || 'null');
|
|
} catch (err) {
|
|
values = null;
|
|
}
|
|
openDialog(opener.getAttribute('data-ui-dialog'), values, opener);
|
|
return;
|
|
}
|
|
// A click on the backdrop, which lands on the dialog element itself
|
|
// outside its box, closes the dialog
|
|
var dialog = e.target;
|
|
if (dialog.matches && dialog.matches('dialog.ui_dialog[open]')) {
|
|
var rect = dialog.getBoundingClientRect();
|
|
if (e.clientX < rect.left || e.clientX > rect.right ||
|
|
e.clientY < rect.top || e.clientY > rect.bottom) {
|
|
dialog.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
})();
|