Files
itflow/js/settings.js

433 lines
18 KiB
JavaScript

/*
* Admin > Settings (admin/settings*.php, functions/settings.php), in the
* browser:
*
* form[data-settings-form] once something is changed, the save bar says
* so and leaving the page asks first; Undo
* changes puts it back. A form sent back with
* something wrong counts as changed.
* #setting=<name> that setting is scrolled to, focused and lit
* up - on its tab, when the page has tabs
* [data-settings-find] Find a setting, on the overview
* [data-settings-preview=<id>] a picked picture shown in the <img> with
* that id before it is sent
* [data-settings-theme] a theme colour shown across the page as it
* is picked
* [data-settings-sample] money and the time now, as the picked
* language, currency and timezone write them
* [data-settings-next] the number the next ticket, invoice... gets
* [data-settings-depends-on=name] faded while that switch is off
*
* Loaded on every admin page (includes/footer.php); it does nothing where
* none of these are.
*/
(function () {
'use strict';
// A form's fields as one string - what was there when the page opened, against what is there now
function serialize(form) {
var parts = [];
Array.prototype.forEach.call(form.elements, function (el) {
if (!el.name || el.disabled || el.name === 'csrf_token' || ['submit', 'button', 'reset'].indexOf(el.type) !== -1) {
return;
}
if (el.type === 'file') {
parts.push(el.name + '=' + (el.files && el.files.length ? el.files[0].name + ':' + el.files[0].size : ''));
} else if (el.type === 'checkbox' || el.type === 'radio') {
if (el.checked) {
parts.push(el.name + '=' + el.value);
}
} else if (el.multiple) {
parts.push(el.name + '=' + Array.prototype.map.call(el.selectedOptions, function (o) { return o.value; }).join(','));
} else {
parts.push(el.name + '=' + el.value);
}
});
return parts.join('&');
}
function initForm(form) {
var bar = form.querySelector('[data-settings-savebar]');
var refused = !!form.querySelector('[data-field-error]');
var initial = null;
var sending = false;
function dirty() {
return refused || (initial !== null && serialize(form) !== initial);
}
function show() {
var is = dirty();
form.classList.toggle('is-dirty', is);
if (bar) {
bar.classList.toggle('is-dirty', is);
}
return is;
}
// After the pickers, the phone field and the rest have set themselves up
setTimeout(function () {
initial = serialize(form);
show();
}, 0);
form.addEventListener('input', show);
form.addEventListener('change', show);
form.addEventListener('submit', function (e) {
if (!e.defaultPrevented) {
sending = true;
}
});
form.addEventListener('reset', function (e) {
// Sent back with something wrong, the form's own values are the ones typed - back to what is saved
if (refused) {
e.preventDefault();
sending = true;
window.location.href = window.location.pathname + window.location.search;
return;
}
setTimeout(function () {
form.querySelectorAll('select').forEach(function (select) {
if (select.itflowSelect) {
select.itflowSelect.setValue(select.multiple ? Array.prototype.map.call(select.selectedOptions, function (o) { return o.value; }) : select.value);
}
});
form.querySelectorAll('input[data-itflow-phone]').forEach(function (input) {
var hidden = form.querySelector('input[name="' + input.dataset.itflowPhone + '"]');
var instance = window.intlTelInput && window.intlTelInput.getInstance ? window.intlTelInput.getInstance(input) : null;
if (instance && hidden && typeof isoForDialCode === 'function') {
var iso = isoForDialCode(hidden.value, '');
if (iso) {
instance.setSelectedCountry(iso);
}
hidden.value = hidden.defaultValue;
}
});
form.dispatchEvent(new CustomEvent('settings:reset'));
show();
}, 0);
});
window.addEventListener('beforeunload', function (e) {
if (!sending && dirty()) {
e.preventDefault();
e.returnValue = '';
}
});
}
// #setting=<name> - the setting, scrolled to and lit up
function goToSetting() {
var match = /^#setting=([A-Za-z0-9_-]+)$/.exec(window.location.hash);
if (!match) {
return;
}
var name = match[1];
var target = document.querySelector('[data-setting="' + name + '"]');
if (!target) {
var field = document.querySelector('[name="' + name + '"]');
target = field ? (field.closest('.mb-3, .form-check, .col-md-6, .col-sm-6, .col') || field) : null;
}
if (!target) {
return;
}
var pane = target.closest('.tab-pane');
if (pane && !pane.classList.contains('active') && window.bootstrap) {
var tab = document.querySelector('[data-bs-target="#' + pane.id + '"]');
if (tab) {
window.bootstrap.Tab.getOrCreateInstance(tab).show();
}
}
setTimeout(function () {
target.scrollIntoView({ block: 'center' });
target.classList.add('itflow-settings-lit');
var input = target.querySelector('input:not([type="hidden"]), select, textarea');
if (input && input.offsetParent !== null) {
(input.itflowSelect && input.itflowSelect.control ? input.itflowSelect.control : input).focus({ preventScroll: true });
}
setTimeout(function () {
target.classList.remove('itflow-settings-lit');
}, 2600);
}, 150);
}
// Find a setting - every page and setting, searched as you type
function initFind(box) {
var input = box.querySelector('[data-settings-find-input]');
var results = box.querySelector('[data-settings-results]');
var index = [];
try {
index = JSON.parse(box.querySelector('[data-settings-index]').textContent);
} catch (err) {
index = [];
}
var active = -1;
function score(entry, terms) {
var label = entry.label.toLowerCase();
var where = entry.where.toLowerCase();
var total = 0;
for (var i = 0; i < terms.length; i++) {
var term = terms[i];
if (label.indexOf(term) === 0) {
total += 6;
} else if (label.indexOf(' ' + term) !== -1) {
total += 5;
} else if (label.indexOf(term) !== -1) {
total += 4;
} else if (where.indexOf(term) !== -1) {
total += 2;
} else if (entry.words.indexOf(term) !== -1) {
total += 1;
} else {
return 0;
}
}
return total + (entry.page ? 0.5 : 0);
}
// The label, with what was typed marked in it
function marked(text, terms) {
var span = document.createElement('span');
var lower = text.toLowerCase();
var at = -1;
var length = 0;
terms.forEach(function (term) {
var i = lower.indexOf(term);
if (i !== -1 && (at === -1 || term.length > length)) {
at = i;
length = term.length;
}
});
if (at === -1) {
span.textContent = text;
return span;
}
span.appendChild(document.createTextNode(text.slice(0, at)));
var mark = document.createElement('mark');
mark.textContent = text.slice(at, at + length);
span.appendChild(mark);
span.appendChild(document.createTextNode(text.slice(at + length)));
return span;
}
function light() {
results.querySelectorAll('[data-settings-result]').forEach(function (item, i) {
item.classList.toggle('active', i === active);
});
}
function render() {
var query = input.value.toLowerCase().trim();
results.innerHTML = '';
active = -1;
if (!query) {
results.hidden = true;
return;
}
var terms = query.split(/\s+/);
var found = index.map(function (entry) {
return [score(entry, terms), entry];
}).filter(function (pair) {
return pair[0] > 0;
}).sort(function (a, b) {
return b[0] - a[0];
}).slice(0, 12);
if (!found.length) {
var none = document.createElement('div');
none.className = 'list-group-item small text-body-secondary';
none.setAttribute('data-settings-none', '');
none.textContent = 'No setting matches "' + input.value.trim() + '".';
results.appendChild(none);
}
found.forEach(function (pair) {
var entry = pair[1];
var item = document.createElement('a');
item.className = 'list-group-item list-group-item-action d-flex gap-3 align-items-center';
item.href = entry.url;
item.setAttribute('data-settings-result', '');
var icon = document.createElement('i');
icon.className = 'fas fa-fw text-body-secondary ' + entry.icon;
var text = document.createElement('span');
text.className = 'min-w-0';
var label = marked(entry.label, terms);
label.className = 'd-block fw-semibold';
var where = document.createElement('span');
where.className = 'd-block small text-body-secondary';
where.textContent = entry.where;
text.appendChild(label);
text.appendChild(where);
item.appendChild(icon);
item.appendChild(text);
results.appendChild(item);
});
results.hidden = false;
active = found.length ? 0 : -1;
light();
}
input.addEventListener('input', render);
input.addEventListener('keydown', function (e) {
var items = results.querySelectorAll('[data-settings-result]');
if (e.key === 'ArrowDown' && items.length) {
e.preventDefault();
active = (active + 1) % items.length;
light();
} else if (e.key === 'ArrowUp' && items.length) {
e.preventDefault();
active = (active - 1 + items.length) % items.length;
light();
} else if (e.key === 'Enter' && items[active]) {
e.preventDefault();
window.location.href = items[active].href;
} else if (e.key === 'Escape') {
input.value = '';
render();
}
});
}
// A picked picture, shown before it is sent
function initPreview(input) {
var img = document.getElementById(input.dataset.settingsPreview);
if (!img) {
return;
}
var original = img.getAttribute('src');
var empty = img.parentNode.querySelector('[data-settings-preview-empty]');
var url = null;
function show() {
if (url) {
URL.revokeObjectURL(url);
url = null;
}
var file = input.files && input.files[0];
if (file && /^image\//.test(file.type)) {
url = URL.createObjectURL(file);
img.src = url;
img.hidden = false;
} else if (original) {
img.src = original;
} else {
img.removeAttribute('src');
img.hidden = true;
}
if (empty) {
empty.hidden = !img.hidden;
}
}
input.addEventListener('change', show);
if (input.form) {
input.form.addEventListener('settings:reset', show);
}
}
// A theme colour shown across the page as it is picked
function initTheme(form) {
function apply() {
var picked = form.querySelector('[data-settings-theme]:checked');
if (!picked) {
return;
}
Array.prototype.slice.call(document.body.classList).forEach(function (name) {
if (name.indexOf('theme-') === 0) {
document.body.classList.remove(name);
}
});
document.body.classList.add('theme-' + picked.value);
document.body.setAttribute('data-lte-primary', picked.value);
}
form.addEventListener('change', function (e) {
if (e.target.matches('[data-settings-theme]')) {
apply();
}
});
form.addEventListener('settings:reset', apply);
}
// Money and the time now, as the picked language, currency and timezone write them
function initSample(form) {
var locale = form.querySelector('[name="company_locale"]');
var currency = form.querySelector('[name="company_currency"]');
var timezone = form.querySelector('[name="config_timezone"]');
var sample = form.querySelector('[data-settings-sample]');
if (!locale || !currency || !timezone || !sample) {
return;
}
function draw() {
var tag = (locale.value || '').replace(/_/g, '-');
try {
sample.querySelector('[data-settings-sample-money]').textContent = new Intl.NumberFormat(tag, { style: 'currency', currency: currency.value }).format(1234.56);
sample.querySelector('[data-settings-sample-date]').textContent = new Intl.DateTimeFormat(tag, { dateStyle: 'full', timeStyle: 'short', timeZone: timezone.value || undefined }).format(new Date());
sample.hidden = false;
} catch (err) {
sample.hidden = true;
}
}
[locale, currency, timezone].forEach(function (select) {
select.addEventListener('change', draw);
});
form.addEventListener('settings:reset', draw);
draw();
}
// The number the next one will get - its prefix and number as they are typed
function initNext(box) {
var names = box.dataset.settingsNext.split(',');
var form = box.closest('form');
var prefix = form ? form.querySelector('[name="' + names[0] + '"]') : null;
var number = form ? form.querySelector('[name="' + names[1] + '"]') : null;
var out = box.querySelector('[data-settings-next-number]');
if (!prefix || !number || !out) {
return;
}
var clash = box.querySelector('[data-settings-clash]');
var inUse = parseInt(box.dataset.settingsInUseNumber || '0', 10);
function draw() {
out.textContent = prefix.value + number.value;
if (clash) {
// Only known for the prefix as saved - another one is checked on saving
clash.hidden = !(prefix.value === box.dataset.settingsInUsePrefix && number.value !== '' && parseInt(number.value, 10) <= inUse);
}
}
prefix.addEventListener('input', draw);
number.addEventListener('input', draw);
form.addEventListener('settings:reset', draw);
}
// A switch whose fields mean nothing while it is off - shown faded, as a hint
function initDepends(group) {
var form = group.closest('form');
var toggle = form ? form.querySelector('[name="' + group.dataset.settingsDependsOn + '"]') : null;
if (!toggle) {
return;
}
function sync() {
group.classList.toggle('is-off', !toggle.checked);
}
toggle.addEventListener('change', sync);
form.addEventListener('settings:reset', sync);
sync();
}
function init() {
document.querySelectorAll('[data-settings-next]').forEach(initNext);
document.querySelectorAll('[data-settings-depends-on]').forEach(initDepends);
document.querySelectorAll('form[data-settings-form]').forEach(initForm);
document.querySelectorAll('[data-settings-find]').forEach(initFind);
document.querySelectorAll('input[type="file"][data-settings-preview]').forEach(initPreview);
document.querySelectorAll('form[data-settings-theme-form]').forEach(initTheme);
document.querySelectorAll('form[data-settings-sample-form]').forEach(initSample);
goToSetting();
}
window.addEventListener('hashchange', goToSetting);
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();