kithkin/resources/js/app.js

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);
});