mirror of
https://github.com/itflow-org/itflow
synced 2026-11-11 07:01:16 +00:00
276 lines
12 KiB
JavaScript
276 lines
12 KiB
JavaScript
// New Category and Edit Category (admin/modals/category/category_form.php).
|
|
// The preview shows the category as it will look on records while its name,
|
|
// colour and icon change - an asset status as its badge, anything else its
|
|
// icon (or a dot) in its colour, as categoryChip() in functions/categories.php
|
|
// draws it. "None" is a choice for both. A swatch, the colour picker or a
|
|
// typed hex sets the colour; the search narrows the icons, and a whole Font
|
|
// Awesome icon name that isn't in the list can be used too. Renaming a kind
|
|
// held by name says the records will be renamed with it. The server checks
|
|
// it all again.
|
|
//
|
|
// Opened from the + beside a picker in a form (categoryAddButtonHtml() in
|
|
// functions/categories.php - js/app.js notes which picker), New Category
|
|
// saves in the background: the new category goes into that picker, picked,
|
|
// and the window closes, leaving the form underneath as it was. A refusal
|
|
// (a name already taken) shows in the window.
|
|
//
|
|
// Wrapped in an IIFE because a modal can be opened, closed and opened again
|
|
// in one page load, which re-runs this file.
|
|
|
|
(function () {
|
|
|
|
// Dark or white text on a #rrggbb colour - tagTextColor() in functions/tags.php
|
|
function textColor(value) {
|
|
const channel = function (hex) {
|
|
const c = parseInt(hex, 16) / 255;
|
|
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
};
|
|
const luminance = 0.2126 * channel(value.substr(1, 2)) + 0.7152 * channel(value.substr(3, 2)) + 0.0722 * channel(value.substr(5, 2));
|
|
return luminance > 0.4 ? '#212529' : '#fff';
|
|
}
|
|
|
|
// Whether Font Awesome draws an icon by this name
|
|
function iconExists(probe, name) {
|
|
probe.className = 'fas fa-' + name;
|
|
const content = window.getComputedStyle(probe, '::before').content;
|
|
return content !== '' && content !== 'none' && content !== 'normal';
|
|
}
|
|
|
|
// #rrggbb from what was typed - '' for none, null when it isn't a colour yet
|
|
function hexColor(value) {
|
|
value = String(value).trim().toLowerCase();
|
|
if (value === '') {
|
|
return '';
|
|
}
|
|
if (value.charAt(0) !== '#') {
|
|
value = '#' + value;
|
|
}
|
|
if (/^#[0-9a-f]{3}$/.test(value)) {
|
|
value = '#' + value[1] + value[1] + value[2] + value[2] + value[3] + value[3];
|
|
}
|
|
return /^#[0-9a-f]{6}$/.test(value) ? value : null;
|
|
}
|
|
|
|
document.querySelectorAll('[data-category-form]:not([data-wired])').forEach(function (form) {
|
|
form.setAttribute('data-wired', '1');
|
|
|
|
const preview = form.querySelector('[data-category-preview]');
|
|
// An asset status's badge - its built-in tone when it has no colour of its own
|
|
const badge = preview.dataset.categoryBadge || '';
|
|
const name = form.querySelector('[data-category-name]');
|
|
const nameWas = form.dataset.categoryNameWas || '';
|
|
const renameNote = form.querySelector('[data-category-rename-note]');
|
|
const color = form.querySelector('[data-category-color]');
|
|
const pick = form.querySelector('[data-category-color-pick]');
|
|
const hex = form.querySelector('[data-category-color-hex]');
|
|
const swatches = form.querySelectorAll('[data-category-swatch]');
|
|
const icon = form.querySelector('[data-category-icon]');
|
|
const search = form.querySelector('[data-category-icon-search]');
|
|
const choices = form.querySelectorAll('[data-category-icon-choice]');
|
|
const custom = form.querySelector('[data-category-icon-custom]');
|
|
const none = form.querySelector('[data-category-icon-none]');
|
|
const probe = document.createElement('i');
|
|
probe.style.position = 'absolute';
|
|
probe.style.visibility = 'hidden';
|
|
form.appendChild(probe);
|
|
|
|
// Redraw the preview from the fields
|
|
function draw() {
|
|
const value = color.value;
|
|
const label = name.value.trim() || 'New category';
|
|
if (badge) {
|
|
const pill = preview.querySelector('.badge');
|
|
pill.textContent = label;
|
|
pill.className = 'badge rounded-pill fw-normal itflow-category-badge' + (value ? '' : ' text-bg-' + badge);
|
|
pill.style.backgroundColor = value;
|
|
pill.style.color = value ? textColor(value) : '';
|
|
return;
|
|
}
|
|
// Its icon in its colour, or a dot of it
|
|
const old = preview.querySelector('.itflow-category-mark');
|
|
const mark = document.createElement(icon.value ? 'i' : 'span');
|
|
mark.className = icon.value ? 'fas fa-fw fa-' + icon.value + ' itflow-category-mark' : 'itflow-category-dot itflow-category-mark';
|
|
mark.setAttribute('aria-hidden', 'true');
|
|
mark.style[icon.value ? 'color' : 'backgroundColor'] = value;
|
|
old.replaceWith(mark);
|
|
preview.querySelector('.itflow-category-name').textContent = label;
|
|
}
|
|
|
|
function setColor(value, from) {
|
|
value = hexColor(value);
|
|
if (value === null) {
|
|
return;
|
|
}
|
|
color.value = value;
|
|
if (from !== pick && value !== '') {
|
|
pick.value = value;
|
|
}
|
|
if (from !== hex) {
|
|
hex.value = value;
|
|
}
|
|
swatches.forEach(function (swatch) {
|
|
const on = swatch.dataset.categorySwatch === value;
|
|
swatch.classList.toggle('active', on);
|
|
swatch.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
});
|
|
draw();
|
|
}
|
|
|
|
function setIcon(value) {
|
|
icon.value = value;
|
|
choices.forEach(function (choice) {
|
|
const on = choice.dataset.categoryIconChoice === value;
|
|
choice.classList.toggle('active', on);
|
|
choice.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
});
|
|
custom.classList.toggle('active', value !== '' && custom.dataset.icon === value);
|
|
draw();
|
|
}
|
|
|
|
function filterIcons() {
|
|
const term = search.value.trim().toLowerCase().replace(/^(fa[srb]?\s+)?fa-/, '').replace(/\s+/g, '-');
|
|
let shown = 0;
|
|
let exact = false;
|
|
choices.forEach(function (choice) {
|
|
const value = choice.dataset.categoryIconChoice;
|
|
// "None" stays while nothing is searched
|
|
const match = term === '' || (value !== '' && value.indexOf(term) !== -1);
|
|
choice.hidden = !match;
|
|
shown += match ? 1 : 0;
|
|
exact = exact || value === term;
|
|
});
|
|
const usable = term !== '' && !exact && /^[a-z0-9-]+$/.test(term) && iconExists(probe, term);
|
|
custom.hidden = !usable;
|
|
if (usable) {
|
|
custom.dataset.icon = term;
|
|
custom.querySelector('i').className = 'fas fa-fw fa-' + term + ' me-1';
|
|
custom.querySelector('span').textContent = 'Use ' + term;
|
|
custom.classList.toggle('active', icon.value === term);
|
|
}
|
|
none.hidden = shown > 0 || usable;
|
|
return shown;
|
|
}
|
|
|
|
name.addEventListener('input', function () {
|
|
draw();
|
|
if (renameNote) {
|
|
renameNote.hidden = name.value.trim() === '' || name.value.trim() === nameWas;
|
|
}
|
|
});
|
|
swatches.forEach(function (swatch) {
|
|
swatch.addEventListener('click', function () {
|
|
setColor(swatch.dataset.categorySwatch);
|
|
});
|
|
});
|
|
pick.addEventListener('input', function () {
|
|
setColor(pick.value, pick);
|
|
});
|
|
hex.addEventListener('input', function () {
|
|
setColor(hex.value, hex);
|
|
});
|
|
hex.addEventListener('change', function () {
|
|
hex.value = color.value;
|
|
});
|
|
// Enter in a colour or search box picks, rather than saving
|
|
hex.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
hex.value = color.value;
|
|
}
|
|
});
|
|
|
|
choices.forEach(function (choice) {
|
|
choice.addEventListener('click', function () {
|
|
setIcon(choice.dataset.categoryIconChoice);
|
|
});
|
|
});
|
|
custom.addEventListener('click', function () {
|
|
setIcon(custom.dataset.icon);
|
|
});
|
|
search.addEventListener('input', filterIcons);
|
|
search.addEventListener('keydown', function (e) {
|
|
if (e.key !== 'Enter') {
|
|
return;
|
|
}
|
|
e.preventDefault();
|
|
const visible = Array.prototype.filter.call(choices, function (choice) {
|
|
return !choice.hidden && choice.dataset.categoryIconChoice !== '';
|
|
});
|
|
if (!custom.hidden) {
|
|
setIcon(custom.dataset.icon);
|
|
} else if (visible.length === 1) {
|
|
setIcon(visible[0].dataset.categoryIconChoice);
|
|
}
|
|
});
|
|
|
|
// From the + beside a picker - saved in the background, into that picker
|
|
const inline = form.closest('form[data-category-inline]');
|
|
if (!inline) {
|
|
return;
|
|
}
|
|
const picker = window.itflowCategoryTarget || null;
|
|
window.itflowCategoryTarget = null;
|
|
const error = inline.querySelector('[data-category-error]');
|
|
inline.addEventListener('submit', function (e) {
|
|
if (!picker || !picker.isConnected) {
|
|
// Nothing to put it in - a plain post, back to the page
|
|
const flag = inline.querySelector('input[name="inline"]');
|
|
if (flag) {
|
|
flag.remove();
|
|
}
|
|
return;
|
|
}
|
|
e.preventDefault();
|
|
const button = inline.querySelector('button[name="add_category"]');
|
|
button.disabled = true;
|
|
const data = new FormData(inline);
|
|
data.append('add_category', '1');
|
|
fetch(inline.getAttribute('action'), { method: 'POST', body: data, credentials: 'same-origin', headers: { 'Accept': 'application/json' } })
|
|
.then(function (res) {
|
|
if (!res.ok) {
|
|
throw new Error('HTTP ' + res.status);
|
|
}
|
|
return res.json();
|
|
})
|
|
.then(function (result) {
|
|
if (!result.ok) {
|
|
throw new Error(result.error || 'It was not saved');
|
|
}
|
|
// Picked in the picker - an option already there for it is used as it is
|
|
let option = Array.prototype.find.call(picker.options, function (o) {
|
|
return o.value === result.value;
|
|
});
|
|
if (!option) {
|
|
const holder = document.createElement('select');
|
|
holder.innerHTML = result.option;
|
|
option = holder.options[0];
|
|
picker.appendChild(option);
|
|
}
|
|
if (!picker.multiple) {
|
|
Array.prototype.forEach.call(picker.options, function (o) {
|
|
o.selected = false;
|
|
});
|
|
}
|
|
option.selected = true;
|
|
picker.dispatchEvent(new Event('input', { bubbles: true }));
|
|
picker.dispatchEvent(new Event('change', { bubbles: true }));
|
|
const modal = inline.closest('.modal');
|
|
if (modal && window.bootstrap) {
|
|
bootstrap.Modal.getOrCreateInstance(modal).hide();
|
|
}
|
|
if (typeof itflowToast === 'function') {
|
|
itflowToast(result.message, 'success');
|
|
}
|
|
})
|
|
.catch(function (err) {
|
|
button.disabled = false;
|
|
if (error) {
|
|
error.textContent = err.message.indexOf('HTTP ') === 0 ? 'It could not be saved - reload the page and try again.' : err.message;
|
|
error.hidden = false;
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
})();
|