/**
* @package Regular Labs Library
* @version 25.6.10828
*
* @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();
})();