/** * @package Regular Labs Library * @version 25.7.12430 * * @author Peter van Westen <[email protected]> * @link https://regularlabs.com * @copyright Copyright © 2025 Regular Labs All Rights Reserved * @license GNU General Public License version 2 or later */ (function(window, document, Math) { var picker, currentEl, oldColor; // Default settings var settings = { el : '[data-rl-mini-colors]', parent : null, wrap : true, margin : 2, swatches : [], swatchesOnly: false, alpha : true, autoClose : false, a11y: { open : 'Open color picker', swatch: 'Color swatch', } }; /** * Configure the color picker. * @param {object} options Configuration options. */ function configure(options) { if (typeof options !== 'object') { return; } for (var key in options) { switch (key) { case 'el': bindFields(options.el); if (options.wrap !== false) { wrapFields(options.el); } break; case 'parent': settings.parent = document.querySelector(options.parent); if (settings.parent) { settings.parent.appendChild(picker); } break; case 'margin': options.margin *= 1; settings.margin = ! isNaN(options.margin) ? options.margin : settings.margin; break; case 'wrap': if (options.el && options.wrap) { wrapFields(options.el); } break; case 'swatches': if (Array.isArray(options.swatches)) { (function() { var swatches = []; options.swatches.forEach(function(swatch, i) { swatches.push("<button id=\"rl-mini-colors-swatch-" + i + "\" class=\"rl-mini-colors-button\" aria-labelledby=\"rl-mini-colors-swatch-label rl-mini-colors-swatch-" + i + "\" style=\"color: " + swatch + ";\">" + swatch + "</button>"); }); if (swatches.length) { getEl('rl-mini-colors-swatches').innerHTML = "<div>" + swatches.join('') + "</div>"; } })(); } break; case 'swatchesOnly': settings.swatchesOnly = !! options.swatchesOnly; picker.setAttribute('data-minimal', settings.swatchesOnly); if (settings.swatchesOnly) { settings.autoClose = true; } break; case 'a11y': var labels = options.a11y; var update = false; if (typeof labels === 'object') { for (var label in labels) { if (labels[label] && settings.a11y[label]) { settings.a11y[label] = labels[label]; update = true; } } } if (update) { var openLabel = getEl('rl-mini-colors-open-label'); var swatchLabel = getEl('rl-mini-colors-swatch-label'); openLabel.innerHTML = settings.a11y.open; swatchLabel.innerHTML = settings.a11y.swatch; } default: settings[key] = options[key]; } } } /** * Bind the color picker to input fields that match the selector. * @param {string} selector One or more selectors pointing to input fields. */ function bindFields(selector) { // Show the color picker on click on the input fields that match the selector addListener(document, 'click', selector, function(event) { var parent = settings.parent; var coords = event.target.getBoundingClientRect(); var scrollY = window.scrollY; var reposition = {left: false, top: false}; var offset = {x: 0, y: 0}; var left = coords.x; var top = scrollY + coords.y + coords.height + settings.margin; currentEl = event.target; picker.classList.add('rl-mini-colors-open'); var pickerWidth = picker.offsetWidth; var pickerHeight = picker.offsetHeight; // If the color picker is inside a custom container // set the position relative to it if (parent) { var style = window.getComputedStyle(parent); var marginTop = parseFloat(style.marginTop); var borderTop = parseFloat(style.borderTopWidth); offset = parent.getBoundingClientRect(); offset.y += borderTop + scrollY; left -= offset.x; top -= offset.y; if (left + pickerWidth > parent.clientWidth) { left += coords.width - pickerWidth; reposition.left = true; } if (top + pickerHeight > parent.clientHeight - marginTop) { top -= coords.height + pickerHeight + settings.margin * 2; reposition.top = true; } top += parent.scrollTop; // Otherwise set the position relative to the whole document } else { if (left + pickerWidth > document.documentElement.clientWidth) { left += coords.width - pickerWidth; reposition.left = true; } if (top + pickerHeight - scrollY > document.documentElement.clientHeight) { top = scrollY + coords.y - pickerHeight - settings.margin; reposition.top = true; } } picker.classList.toggle('rl-mini-colors-left', reposition.left); picker.classList.toggle('rl-mini-colors-top', reposition.top); picker.style.left = left + "px"; picker.style.top = top + "px"; deselectRow(currentEl); }); } function deselectRow(el) { const tr = el.closest('tr'); if ( ! tr) { return; } const input = tr.querySelector('.form-check-input'); if ( ! input) { return; } input.checked = false; } /** * Wrap the linked input fields in a div that adds a color preview. * @param {string} selector One or more selectors pointing to input fields. */ function wrapFields(selector) { document.querySelectorAll(selector).forEach(function(field) { var parentNode = field.parentNode; if ( ! parentNode.classList.contains('rl-mini-colors-field')) { var wrapper = document.createElement('div'); wrapper.innerHTML = "<button class=\"rl-mini-colors-button\" aria-labelledby=\"rl-mini-colors-open-label\"></button>"; parentNode.insertBefore(wrapper, field); wrapper.setAttribute('class', 'rl-mini-colors-field'); wrapper.style.color = field.value; wrapper.appendChild(field); } }); } /** * Close the color picker. * @param {boolean} [revert] If true, revert the color to the original value. */ function closePicker(color) { if ( ! currentEl) { return; } picker.classList.remove('rl-mini-colors-open'); currentEl = null; } /** * Copy the active color to the linked input field. * @param {number} [color] Color value to override the active color. */ function pickColor(color) { if ( ! currentEl) { return; } currentEl.value = color; currentEl.dispatchEvent(new Event('input', {bubbles: true})); currentEl.dispatchEvent(new Event('change', {bubbles: true})); const parent = currentEl.parentNode; if (parent.classList.contains('rl-mini-colors-field')) { parent.style.color = color; } } /** * Init the color picker. */ function init() { // Render the UI picker = document.createElement('div'); picker.setAttribute('id', 'rl-mini-colors-picker'); picker.className = 'rl-mini-colors-picker'; picker.innerHTML = '<div id="rl-mini-colors-swatches" class="rl-mini-colors-swatches"></div>' + ("<span id=\"rl-mini-colors-open-label\" hidden>" + settings.a11y.open + "</span>") + ("<span id=\"rl-mini-colors-swatch-label\" hidden>" + settings.a11y.swatch + "</span>"); // Append the color picker to the DOM document.body.appendChild(picker); // Bind the picker to the default selector bindFields(settings.el); wrapFields(settings.el); addListener(picker, 'click', '.rl-mini-colors-swatches .rl-mini-colors-button', (event) => { pickColor(event.target.textContent); closePicker(event.target.textContent); }); addListener(document, 'click', '.rl-mini-colors-field .rl-mini-colors-button', (event) => { event.target.nextElementSibling.dispatchEvent(new Event('click', {bubbles: true})); }); addListener(document, 'mousedown', (event) => { if (event.target.classList.contains('rl-mini-colors') || event.target.classList.contains('rl-mini-colors-picker') || event.target.closest('.rl-mini-colors-picker')) { return; } closePicker(); }); } /** * Shortcut for getElementById to optimize the minified JS. * @param {string} id The element id. * @return {object} The DOM element with the provided id. */ function getEl(id) { return document.getElementById(id); } /** * Shortcut for addEventListener to optimize the minified JS. * @param {object} context The context to which the listener is attached. * @param {string} type Event type. * @param {(string|function)} selector Event target if delegation is used, event handler if not. * @param {function} [fn] Event handler if delegation is used. */ function addListener(context, type, selector, fn) { var matches = Element.prototype.matches || Element.prototype.msMatchesSelector; // Delegate event to the target of the selector if (typeof selector === 'string') { context.addEventListener(type, function(event) { if (matches.call(event.target, selector)) { fn.call(event.target, event); } }); // If the selector is not a string then it's a function // in which case we need regular event listener } else { fn = selector; context.addEventListener(type, fn); } } /** * Call a function only when the DOM is ready. * @param {function} fn The function to call. * @param {array} [args] Arguments to pass to the function. */ function DOMReady(fn, args) { args = args !== undefined ? args : []; if (document.readyState !== 'loading') { fn.apply(void 0, args); } else { document.addEventListener('DOMContentLoaded', function() { fn.apply(void 0, args); }); } } // Polyfill for Nodelist.forEach if (NodeList !== undefined && NodeList.prototype && ! NodeList.prototype.forEach) { NodeList.prototype.forEach = Array.prototype.forEach; } // Expose the color picker to the global scope window.RegularLabs_MiniColors = function() { var methods = { set : configure, wrap : wrapFields, close: closePicker }; function RegularLabs_MiniColors(options) { DOMReady(function() { if (options) { if (typeof options === 'string') { bindFields(options); } else { configure(options); } } }); } var _loop = function _loop(key) { RegularLabs_MiniColors[key] = function() { for (var _len = arguments.length, args = new Array(_len), _key2 = 0; _key2 < _len; _key2++) { args[_key2] = arguments[_key2]; } DOMReady(methods[key], args); }; }; for (var key in methods) { _loop(key); } return RegularLabs_MiniColors; }(); // Init the color picker when the DOM is ready DOMReady(init); })(window, document, Math); (function() { 'use strict'; window.RegularLabs = window.RegularLabs || {}; window.RegularLabs.MiniColors = window.RegularLabs.MiniColors || { init: function() { const minicolors = document.querySelectorAll('div.rl-mini-colors'); const options = Joomla.getOptions ? Joomla.getOptions('rl_minicolors', {}) : Joomla.optionsStorage.rl_minicolors || {}; minicolors.forEach((minicolor) => { const field = minicolor.querySelector('input'); RegularLabs_MiniColors({ el : `#${field.id}`, theme : 'default', alpha : false, swatchesOnly: true, swatches : options.swatches }); if ( ! field.dataset['table'] || ! field.dataset['item_id']) { return; } field.addEventListener('change', () => { RegularLabs.MiniColors.save(field.dataset['table'], field.dataset['item_id'], field.dataset['id_column'], field.value, field); }); RegularLabs.MiniColors.setTableRowBackground(field, field.value); }); }, setTableRowBackground: async function(element, color, opacity = .1) { if ( ! element) { return; } const table_row = element.closest('tr'); if ( ! table_row) { return; } const table_cells = table_row.querySelectorAll('td, th'); if ( ! table_cells.length) { return; } const bg_color = RegularLabs.MiniColors.getColorWithOpacity(color, opacity); if (color[0] === '#') { table_cells[0].style.borderLeft = `4px solid ${color}`; } table_cells.forEach((table_cell) => { table_cell.style.backgroundColor = bg_color; }); }, save: async function(table, item_id, id_column, color, element) { let spinner = null; id_column = id_column ? id_column : 'id'; if (element) { spinner = document.createElement('div'); spinner.classList.add('rl-spinner'); element.closest('div.rl-mini-colors-field').append(spinner); RegularLabs.MiniColors.setTableRowBackground(element, color); } const url = 'index.php?option=com_ajax&plugin=regularlabs&format=raw&saveColor=1' + '&table=' + table + '&item_id=' + item_id + '&id_column=' + id_column + '&color=' + encodeURIComponent(color); await RegularLabs.Scripts.loadUrl(url); RegularLabs.MiniColors.saved(spinner); }, saved: function(spinner = null) { if ( ! spinner) { return; } spinner.remove(); }, getColorWithOpacity: function(hex, opacity) { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); if ( ! result) { return 'var(--table-bg)'; } return 'rgba(' + parseInt(result[1], 16) + ',' + parseInt(result[2], 16) + ',' + parseInt(result[3], 16) + ',' + opacity + ')'; } }; RegularLabs.MiniColors.init(); })();