173 lines
4.9 KiB
JavaScript
173 lines
4.9 KiB
JavaScript
import './bootstrap';
|
|
import htmx from 'htmx.org';
|
|
|
|
const SELECTORS = {
|
|
calendarToggle: '.calendar-toggle',
|
|
calendarViewForm: '#calendar-view',
|
|
calendarExpandToggle: '[data-calendar-expand]',
|
|
colorPicker: '[data-colorpicker]',
|
|
colorPickerColor: '[data-colorpicker-color]',
|
|
colorPickerHex: '[data-colorpicker-hex]',
|
|
colorPickerRandom: '[data-colorpicker-random]',
|
|
};
|
|
|
|
/**
|
|
* htmx/global
|
|
*/
|
|
|
|
// make html globally visible to use the devtools and extensions
|
|
window.htmx = htmx;
|
|
|
|
// global htmx config
|
|
htmx.config.historyEnabled = true; // HX-Boost back/forward support
|
|
htmx.logger = console.log; // verbose logging during dev
|
|
|
|
// csrf on htmx requests
|
|
document.addEventListener('htmx:configRequest', (evt) => {
|
|
const token = document.querySelector('meta[name="csrf-token"]')?.content
|
|
if (token) evt.detail.headers['X-CSRF-TOKEN'] = token
|
|
})
|
|
|
|
/**
|
|
* calendar ui
|
|
* progressive enhancement on html form with no js
|
|
*/
|
|
document.addEventListener('change', (event) => {
|
|
const target = event.target;
|
|
|
|
if (target?.matches(SELECTORS.calendarToggle)) {
|
|
const slug = target.value;
|
|
const show = target.checked;
|
|
|
|
document
|
|
.querySelectorAll(`[data-calendar="${slug}"]`)
|
|
.forEach(el => el.classList.toggle('hidden', !show));
|
|
return;
|
|
}
|
|
|
|
const form = target?.form;
|
|
if (!form || form.id !== 'calendar-view') return;
|
|
if (target.name !== 'view') return;
|
|
|
|
form.requestSubmit();
|
|
});
|
|
|
|
/**
|
|
* calendar sidebar expand toggle
|
|
*/
|
|
document.addEventListener('click', (event) => {
|
|
const toggle = event.target.closest(SELECTORS.calendarExpandToggle);
|
|
if (!toggle) return;
|
|
|
|
event.preventDefault();
|
|
|
|
const main = toggle.closest('main');
|
|
if (!main) return;
|
|
|
|
const isExpanded = main.classList.toggle('expanded');
|
|
toggle.setAttribute('aria-pressed', isExpanded ? 'true' : 'false');
|
|
});
|
|
|
|
/**
|
|
* color picker component
|
|
* native <input type="color"> + hex + random palette)
|
|
*/
|
|
function initColorPickers(root = document) {
|
|
const isHex = (v) => /^#?[0-9a-fA-F]{6}$/.test((v || '').trim());
|
|
|
|
const normalize = (v) => {
|
|
let s = (v || '').trim();
|
|
if (!s) return null;
|
|
if (!s.startsWith('#')) s = '#' + s;
|
|
if (!isHex(s)) return null;
|
|
return s.toUpperCase();
|
|
};
|
|
|
|
const pickRandom = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
|
|
|
const wire = (el) => {
|
|
// avoid double-binding when htmx swaps
|
|
if (el.__colorpickerWired) return;
|
|
el.__colorpickerWired = true;
|
|
|
|
const color = el.querySelector(SELECTORS.colorPickerColor);
|
|
const hex = el.querySelector(SELECTORS.colorPickerHex);
|
|
const btn = el.querySelector(SELECTORS.colorPickerRandom);
|
|
|
|
if (!color || !hex) return;
|
|
|
|
let palette = [];
|
|
try {
|
|
palette = JSON.parse(el.getAttribute('data-palette') || '[]');
|
|
} catch {
|
|
palette = [];
|
|
}
|
|
|
|
const setValue = (val) => {
|
|
const n = normalize(val);
|
|
if (!n) return false;
|
|
|
|
color.value = n;
|
|
hex.value = n;
|
|
|
|
// bubble input/change for any listeners (htmx, previews, etc.)
|
|
color.dispatchEvent(new Event('input', { bubbles: true }));
|
|
color.dispatchEvent(new Event('change', { bubbles: true }));
|
|
return true;
|
|
};
|
|
|
|
// init sync from native input
|
|
hex.value = normalize(color.value) || '#000000';
|
|
|
|
// native picker -> hex field
|
|
color.addEventListener('input', () => {
|
|
const n = normalize(color.value);
|
|
if (n) hex.value = n;
|
|
});
|
|
|
|
// hex typing -> native picker (on blur + Enter)
|
|
const commitHex = () => {
|
|
const ok = setValue(hex.value);
|
|
if (!ok) hex.value = normalize(color.value) || hex.value;
|
|
};
|
|
|
|
hex.addEventListener('blur', commitHex);
|
|
hex.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
commitHex();
|
|
}
|
|
});
|
|
|
|
// random button
|
|
if (btn && palette.length) {
|
|
btn.addEventListener('click', (e) => {
|
|
e.preventDefault(); // defensive: never submit, never navigate
|
|
e.stopPropagation();
|
|
|
|
let next = pickRandom(palette);
|
|
if (palette.length > 1) {
|
|
const current = normalize(color.value);
|
|
// avoid re-rolling the same number if possible
|
|
while (normalize(next) === current) next = pickRandom(palette);
|
|
}
|
|
setValue(next);
|
|
});
|
|
}
|
|
};
|
|
|
|
root.querySelectorAll(SELECTORS.colorPicker).forEach(wire);
|
|
}
|
|
|
|
function initUI() {
|
|
initColorPickers();
|
|
}
|
|
|
|
// initial bind
|
|
document.addEventListener('DOMContentLoaded', initUI);
|
|
|
|
// rebind in htmx for swapped content
|
|
document.addEventListener('htmx:afterSwap', (e) => {
|
|
initColorPickers(e.target);
|
|
});
|