function latModeGlyph(m) {
return m === 'light' ? '☀' : m === 'dark' ? '☾' : '◐';
}
function latCycleMode() {
var o = latMode();
var n = o === 'light' ? 'dark' : o === 'dark' ? 'auto' : 'light';
latSetMode(n);
var e = document.getElementById('lat-mode-ico');
if (e) e.textContent = latModeGlyph(n);
}
var latDrawerOpener = null;
function latToggleMenu() {
var m = document.getElementById('lat-menu');
if (!m) return;
var open = !m.classList.contains('is-open');
m.classList.toggle('is-open', open);
var burger = document.querySelector('[data-lat-drawer][aria-controls="lat-menu"]');
if (burger) burger.setAttribute('aria-expanded', open ? 'true' : 'false');
if (open) {
latDrawerOpener = document.activeElement;
var first = m.querySelector('a, button');
if (first) first.focus();
} else if (latDrawerOpener && latDrawerOpener.focus) {
latDrawerOpener.focus();
latDrawerOpener = null;
}
}
document.addEventListener('keydown', function (e) {
var m = document.getElementById('lat-menu');
if (!m || !m.classList.contains('is-open')) return;
if (e.key === 'Escape') { e.preventDefault(); latToggleMenu(); return; }
if (e.key !== 'Tab') return;
var items = m.querySelectorAll('a, button');
if (!items.length) return;
var first = items[0], last = items[items.length - 1];
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
});
document.addEventListener('click', function (e) {
if (!e.target.closest) return;
var dismiss = e.target.closest('[data-lat-dismiss]');
if (dismiss) { latDismissFlash(dismiss); return; }
if (e.target.closest('[data-lat-drawer]')) { latToggleMenu(); return; }
if (e.target.closest('[data-lat-mode]')) { latCycleMode(); }
});
function flashText(toast) {
var label = toast.querySelector('.lat-flash__text');
return label ? label.textContent : '';
}
function latDismissFlash(btn) {
var t = btn.closest('.lat-flash');
if (!t) return;
window.lat.emit(t, 'flash:dismissed', { text: flashText(t) });
t.classList.add('is-leaving');
setTimeout(function () { t.remove(); }, 180);
}
document.addEventListener('htmx:beforeSwap', function (e) {
if (e.detail.xhr.status === 422) {
e.detail.shouldSwap = true;
e.detail.isError = false;
}
});
(function () {
var registry = {};
var controllers = new WeakMap();
var lat = (window.lat = window.lat || {});
lat.widget = function (name, init) {
registry[name] = init;
scan(document);
};
lat.get = function (el) {
var root = el && el.closest ? el.closest('[data-lat-widget]') : null;
return root ? controllers.get(root) : undefined;
};
lat.emit = function (el, name, detail, cancelable) {
return (el || document).dispatchEvent(new CustomEvent('lat:' + name, {
bubbles: true,
cancelable: !!cancelable,
detail: detail || {}
}));
};
lat.on = function (name, handler) {
function listener(e) { handler(e.detail, e); }
document.addEventListener('lat:' + name, listener);
return function () { document.removeEventListener('lat:' + name, listener); };
};
lat.assets = {
ensure: function (url) {
if (document.querySelector('[data-lat-asset="' + url + '"]')) return;
var el;
if (/\.css(\?|$)/.test(url)) {
el = document.createElement('link');
el.rel = 'stylesheet';
el.href = url;
} else {
el = document.createElement('script');
el.src = url;
el.defer = true;
}
el.setAttribute('data-lat-asset', url);
document.head.appendChild(el);
}
};
var unstarted = '[data-lat-widget]:not([data-lat-ready])';
function start(el) {
var init = registry[el.getAttribute('data-lat-widget')];
if (!init) return;
el.setAttribute('data-lat-ready', '1');
var controller = init(el);
if (!controller) return;
controllers.set(el, controller);
if (controller.root && controller.root !== el) {
controller.root.setAttribute('data-lat-widget', el.getAttribute('data-lat-widget'));
controller.root.setAttribute('data-lat-ready', '1');
controllers.set(controller.root, controller);
}
}
function scan(root) {
var scope = root && root.querySelectorAll ? root : document;
if (scope.matches && scope.matches(unstarted)) start(scope);
scope.querySelectorAll(unstarted).forEach(start);
}
lat.scan = scan;
document.addEventListener('DOMContentLoaded', function () {
scan(document);
var e = document.getElementById('lat-mode-ico');
if (e) e.textContent = latModeGlyph(latMode());
});
document.addEventListener('htmx:load', function (ev) { scan(ev.target); });
})();
window.lat.flash = function (text, level) {
var box = document.querySelector('.lat-flashes');
if (!box) {
box = document.createElement('div');
box.className = 'lat-flashes';
box.setAttribute('role', 'status');
box.setAttribute('aria-live', 'polite');
document.body.insertBefore(box, document.body.firstChild);
}
var toast = document.createElement('div');
toast.className = 'lat-flash is-' + (level || 'error');
toast.setAttribute('data-lat-widget', 'flash');
var label = document.createElement('span');
label.className = 'lat-flash__text';
label.textContent = text;
var close = document.createElement('button');
close.type = 'button';
close.className = 'lat-flash__close';
close.innerHTML = '×';
close.addEventListener('click', function () { latDismissFlash(close); });
toast.appendChild(label);
toast.appendChild(close);
box.appendChild(toast);
window.lat.scan(toast);
};
function latRequestFailed() {
window.lat.flash(document.body.getAttribute('data-lat-request-error') || 'Request failed.', 'error');
}
document.addEventListener('htmx:responseError', latRequestFailed);
document.addEventListener('htmx:sendError', latRequestFailed);
(function () {
var inflight = 0;
var bar = null;
var timer = null;
function element() {
if (!bar) {
bar = document.createElement('div');
bar.className = 'lat-progress';
bar.setAttribute('aria-hidden', 'true');
document.body.appendChild(bar);
}
return bar;
}
document.addEventListener('htmx:beforeRequest', function () {
inflight++;
clearTimeout(timer);
var b = element();
b.classList.remove('is-done');
b.classList.add('is-active');
document.body.setAttribute('aria-busy', 'true');
});
document.addEventListener('htmx:afterRequest', function () {
inflight = Math.max(0, inflight - 1);
if (inflight > 0) return;
var b = element();
b.classList.add('is-done');
document.body.removeAttribute('aria-busy');
timer = setTimeout(function () { b.classList.remove('is-active', 'is-done'); }, 220);
});
})();
(function () {
var pending = null;
var asked = null;
function box() {
var el = document.getElementById('lat-confirm');
if (el) return el;
el = document.createElement('div');
el.id = 'lat-confirm';
el.className = 'lat-modal';
el.setAttribute('role', 'dialog');
el.setAttribute('aria-modal', 'true');
el.innerHTML =
'<div class="lat-modal__backdrop" data-lat-close></div>' +
'<div class="lat-modal__panel">' +
'<p class="lat-modal__text"></p>' +
'<div class="lat-modal__actions">' +
'<button type="button" class="lat-btn lat-btn--ghost" data-lat-close></button>' +
'<button type="button" class="lat-btn lat-btn--danger" data-lat-go></button>' +
'</div></div>';
document.body.appendChild(el);
el.addEventListener('click', function (e) {
if (e.target.hasAttribute('data-lat-close')) close();
if (e.target.hasAttribute('data-lat-go')) go();
});
return el;
}
function close() {
if (answer(false)) return;
if (pending) window.lat.emit(pending, 'confirm:cancelled', {});
hide();
}
function hide() {
pending = null;
var el = document.getElementById('lat-confirm');
if (el) el.classList.remove('is-open');
}
window.lat.confirm = function (text, labels, done) {
labels = labels || {};
asked = done;
pending = null;
var modal = box();
modal.querySelector('.lat-modal__text').textContent = text;
modal.querySelector('[data-lat-go]').textContent = labels.go || 'OK';
modal.querySelector('[data-lat-close]:not(.lat-modal__backdrop)').textContent =
labels.cancel || document.body.getAttribute('data-lat-cancel') || 'Cancel';
modal.classList.add('is-open');
modal.querySelector(
labels.focus === 'cancel' ? '.lat-modal__actions [data-lat-close]' : '[data-lat-go]'
).focus();
window.lat.emit(document, 'confirm:opened', { text: text });
};
function answer(yes) {
var done = asked;
asked = null;
if (!done) return false;
hide();
window.lat.emit(document, yes ? 'confirm:confirmed' : 'confirm:cancelled', {});
done(yes);
return true;
}
function go() {
if (answer(true)) return;
var el = pending;
hide();
if (!el) return;
window.lat.emit(el, 'confirm:confirmed', {});
el.setAttribute('data-lat-confirmed', '1');
el.click();
el.removeAttribute('data-lat-confirmed');
}
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') close();
});
document.addEventListener('click', function (e) {
var el = e.target.closest ? e.target.closest('[data-lat-confirm]') : null;
if (!el || el.hasAttribute('data-lat-confirmed')) return;
e.preventDefault();
e.stopPropagation();
pending = el;
var modal = box();
modal.querySelector('.lat-modal__text').textContent = el.getAttribute('data-lat-confirm');
modal.querySelector('[data-lat-go]').textContent = el.textContent.trim() || 'OK';
modal.querySelector('[data-lat-close]:not(.lat-modal__backdrop)').textContent =
document.body.getAttribute('data-lat-cancel') || 'Cancel';
modal.classList.add('is-open');
modal.querySelector('[data-lat-go]').focus();
window.lat.emit(el, 'confirm:opened', { text: el.getAttribute('data-lat-confirm') });
}, true);
})();
(function () {
document.addEventListener('click', function (e) {
if (!e.target.closest) return;
var opener = e.target.closest('[data-lat-dialog]');
if (opener) {
var dialog = document.getElementById(opener.getAttribute('data-lat-dialog'));
if (!dialog || typeof dialog.showModal !== 'function') return;
e.preventDefault();
(opener.getAttribute('data-lat-dialog-set') || '').split(';').forEach(function (pair) {
var at = pair.indexOf('=');
if (at < 1) return;
var field = dialog.querySelector('[name="' + pair.slice(0, at).trim() + '"]');
if (field) field.value = pair.slice(at + 1).trim();
});
dialog.showModal();
var first = dialog.querySelector('[autofocus], input:not([type=hidden]):not([readonly]), select, textarea');
if (first) first.focus();
window.lat.emit(dialog, 'dialog:opened', {});
return;
}
var closer = e.target.closest('[data-lat-dialog-close]');
if (closer) {
var open = closer.closest('dialog');
if (open) open.close();
return;
}
if (e.target.matches && e.target.matches('dialog.lat-dialog')) e.target.close();
});
})();
window.lat.widget('upload', function (root) {
var input = root.querySelector('input[type=file]');
var list = root.querySelector('.lat-upload__list');
var url = root.getAttribute('data-upload-url');
var form = root.closest('form');
if (!input || !list) return;
var words = {
uploading: root.getAttribute('data-lat-uploading') || 'Uploading',
stored: root.getAttribute('data-lat-stored') || 'Stored',
failed: root.getAttribute('data-lat-failed') || 'Could not be stored',
};
var pending = 0;
var failures = 0;
function size(n) {
if (n < 1024) return n + ' B';
if (n < 1048576) return (n / 1024).toFixed(0) + ' KB';
return (n / 1048576).toFixed(1) + ' MB';
}
function row(file) {
var li = document.createElement('li');
li.className = 'lat-upload__item';
li.innerHTML =
'<span class="lat-upload__name"></span><span class="lat-upload__size"></span>' +
(url ? '<span class="lat-upload__bar"><i></i></span><span class="lat-upload__status"></span>' : '');
li.querySelector('.lat-upload__name').textContent = file.name;
li.querySelector('.lat-upload__size').textContent = size(file.size);
list.appendChild(li);
return li;
}
function finish(li, ok, text) {
li.classList.add(ok ? 'is-done' : 'is-failed');
li.querySelector('.lat-upload__status').textContent = text;
if (!ok) failures += 1;
pending -= 1;
window.lat.emit(root, ok ? 'upload:stored' : 'upload:failed', { name: li.querySelector('.lat-upload__name').textContent, reason: text });
if (pending === 0) {
window.lat.emit(root, 'upload:done', { failed: failures });
if (failures === 0) setTimeout(function () { location.reload(); }, 350);
}
}
function post(file, li, bar, status) {
var body = new FormData();
var token = form && form.querySelector('input[name="_csrf"]');
if (token) body.append('_csrf', token.value);
(root.getAttribute('data-upload-fields') || '').split(';').forEach(function (pair) {
var at = pair.indexOf('=');
if (at > 0) body.append(pair.slice(0, at).trim(), pair.slice(at + 1).trim());
});
body.append(input.name || 'file', file, file.name);
var xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', function (e) {
if (e.lengthComputable) bar.style.width = Math.round((e.loaded / e.total) * 100) + '%';
});
xhr.addEventListener('load', function () {
var out = null;
try { out = JSON.parse(xhr.responseText); } catch (e) { out = null; }
var stored = out && out.stored && out.stored[0];
var refused = out && out.refused && out.refused[0];
if (xhr.status === 200 && stored) finish(li, true, words.stored);
else finish(li, false, (refused && refused.reason) || words.failed);
});
xhr.addEventListener('error', function () { finish(li, false, words.failed); });
xhr.open('POST', url);
xhr.setRequestHeader('X-Requested-With', 'fetch');
xhr.send(body);
}
function direct(file, li, bar, status, ask) {
var token = form && form.querySelector('input[name="_csrf"]');
var fields = {};
(root.getAttribute('data-upload-fields') || '').split(';').forEach(function (pair) {
var at = pair.indexOf('=');
if (at > 0) fields[pair.slice(0, at).trim()] = pair.slice(at + 1).trim();
});
var start = new FormData();
if (token) start.append('_csrf', token.value);
start.append('name', file.name);
start.append('content_type', file.type || 'application/octet-stream');
start.append('size', String(file.size));
fetch(ask, { method: 'POST', body: start, credentials: 'same-origin', headers: { 'X-Requested-With': 'fetch' } })
.then(function (r) { return r.ok ? r.json() : { direct: false }; })
.catch(function () { return { direct: false }; })
.then(function (plan) {
if (!plan || !plan.direct || !plan.url) { post(file, li, bar, status); return; }
var xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', function (e) {
if (e.lengthComputable) bar.style.width = Math.round((e.loaded / e.total) * 95) + '%';
});
xhr.addEventListener('load', function () {
if (xhr.status < 200 || xhr.status >= 300) { finish(li, false, words.failed); return; }
var done = new FormData();
if (token) done.append('_csrf', token.value);
done.append('path', plan.path);
done.append('name', file.name);
Object.keys(fields).forEach(function (k) { done.append(k, fields[k]); });
fetch(ask + '/complete', { method: 'POST', body: done, credentials: 'same-origin', headers: { 'X-Requested-With': 'fetch' } })
.then(function (r) { return r.json(); })
.then(function (out) {
var stored = out && out.stored && out.stored[0];
var refused = out && out.refused && out.refused[0];
if (stored) finish(li, true, words.stored);
else finish(li, false, (refused && refused.reason) || words.failed);
})
.catch(function () { finish(li, false, words.failed); });
});
xhr.addEventListener('error', function () { finish(li, false, words.failed); });
xhr.open('PUT', plan.url);
Object.keys(plan.headers || {}).forEach(function (h) { xhr.setRequestHeader(h, plan.headers[h]); });
xhr.send(file);
});
}
function send(file, li) {
var bar = li.querySelector('.lat-upload__bar i');
var status = li.querySelector('.lat-upload__status');
status.textContent = words.uploading;
var ask = root.getAttribute('data-upload-direct');
if (ask) direct(file, li, bar, status, ask); else post(file, li, bar, status);
}
function take(files) {
var picked = Array.prototype.slice.call(files || []);
if (!picked.length) return;
if (!url) {
list.innerHTML = '';
picked.forEach(row);
window.lat.emit(root, 'upload:picked', { count: picked.length });
return;
}
if (!window.lat.emit(root, 'upload:before-send', { count: picked.length }, true)) return;
picked.forEach(function (file) {
pending += 1;
send(file, row(file));
});
}
if (url) {
input.required = false;
var submit = form && form.querySelector('[data-upload-submit]');
if (submit) submit.hidden = true;
}
input.addEventListener('change', function () {
take(input.files);
if (url) input.value = '';
});
['dragenter', 'dragover'].forEach(function (name) {
root.addEventListener(name, function (e) { e.preventDefault(); root.classList.add('is-dragging'); });
});
['dragleave', 'drop'].forEach(function (name) {
root.addEventListener(name, function (e) { e.preventDefault(); root.classList.remove('is-dragging'); });
});
root.addEventListener('drop', function (e) {
var files = e.dataTransfer && e.dataTransfer.files;
if (!files || !files.length) return;
if (url) {
take(files);
} else {
try { input.files = files; } catch (err) { }
take(input.files);
}
});
return { take: take };
});
window.lat.mediaPicker = function (opts) {
var multiple = !!opts.multiple;
var words = {
title: opts.words.title || 'Choose from the library',
use: opts.words.use || 'Use selected',
cancel: opts.words.cancel || document.body.getAttribute('data-lat-cancel') || 'Cancel'
};
var dialog = null;
var chosen = [];
function box() {
if (dialog) return dialog;
dialog = document.createElement('dialog');
dialog.className = 'lat-dialog lat-dialog--pick';
dialog.innerHTML =
'<div class="lat-dialog__head"><h2></h2><button type="button" class="lat-dialog__close" data-lat-dialog-close aria-label="' + words.cancel + '">×</button></div>' +
'<div class="lat-dialog__body lat-dialog__body--flush" data-pick-body></div>' +
'<div class="lat-dialog__actions"><button type="button" class="lat-btn lat-btn--ghost" data-lat-dialog-close></button><button type="button" class="lat-btn lat-btn--primary" data-pick-use hidden></button></div>';
dialog.querySelector('h2').textContent = words.title;
dialog.querySelector('.lat-dialog__actions [data-lat-dialog-close]').textContent = words.cancel;
var use = dialog.querySelector('[data-pick-use]');
use.textContent = words.use;
use.hidden = !multiple;
use.addEventListener('click', function () { finish(chosen); });
document.body.appendChild(dialog);
dialog.addEventListener('click', function (e) {
if (!e.target.closest) return;
var go = e.target.closest('[data-pick-go]');
if (go) { e.preventDefault(); show(go.getAttribute('href')); return; }
var file = e.target.closest('[data-pick-file]');
if (file) {
var item = {
id: file.getAttribute('data-id'),
name: file.getAttribute('data-name'),
kind: file.getAttribute('data-kind'),
thumb: file.getAttribute('data-thumb'),
url: file.getAttribute('data-url'),
is_image: file.getAttribute('data-is-image') === '1',
};
if (!multiple) { finish([item]); return; }
var on = file.getAttribute('aria-pressed') === 'true';
file.setAttribute('aria-pressed', on ? 'false' : 'true');
chosen = on ? chosen.filter(function (c) { return c.id !== item.id; }) : chosen.concat([item]);
}
});
dialog.addEventListener('submit', function (e) {
var form = e.target.closest ? e.target.closest('[data-pick-search]') : null;
if (!form) return;
e.preventDefault();
var params = new URLSearchParams(new FormData(form));
show(form.getAttribute('action') + '?' + params.toString());
});
return dialog;
}
function show(url) {
var body = box().querySelector('[data-pick-body]');
fetch(url, { credentials: 'same-origin', headers: { 'X-Requested-With': 'fetch' } })
.then(function (r) { return r.text(); })
.then(function (html) {
body.innerHTML = html;
chosen.forEach(function (c) {
var el = body.querySelector('[data-pick-file][data-id="' + c.id + '"]');
if (el) el.setAttribute('aria-pressed', 'true');
});
if (!dialog.open) dialog.showModal();
});
}
function finish(items) {
if (items.length) opts.onDone(items);
chosen = [];
if (dialog) dialog.close();
}
return {
open: function (url) { chosen = []; show(url); },
close: function () { if (dialog) dialog.close(); }
};
};
window.lat.widget('media-field', function (root) {
var input = root.querySelector('[data-mediafield-value]');
var tiles = root.querySelector('[data-mediafield-tiles]');
var pickBtn = root.querySelector('[data-mediafield-pick]');
var clearBtn = root.querySelector('[data-mediafield-clear]');
if (!input || !tiles || !pickBtn) return;
var max = parseInt(root.getAttribute('data-max') || '1', 10);
var multiple = max !== 1;
var words = {
select: root.getAttribute('data-lat-select') || 'Select',
replace: root.getAttribute('data-lat-replace') || 'Replace',
use: root.getAttribute('data-lat-use') || 'Use selected',
title: root.getAttribute('data-lat-title') || 'Choose from the library',
cancel: document.body.getAttribute('data-lat-cancel') || 'Cancel',
};
function ids() {
return input.value.split(',').map(function (s) { return s.trim(); }).filter(Boolean);
}
function write(list) {
input.value = list.join(',');
input.dispatchEvent(new Event('change', { bubbles: true }));
pickBtn.textContent = list.length && !multiple ? words.replace : words.select;
if (clearBtn) clearBtn.hidden = list.length === 0;
window.lat.emit(root, 'media:changed', { ids: list.slice() });
}
function tile(item) {
var el = document.createElement('div');
el.className = 'lat-mediafield__tile';
el.setAttribute('data-id', item.id);
el.innerHTML =
'<span class="lat-media-card__thumb"></span>' +
'<span class="lat-media-card__name"></span>' +
'<button type="button" class="lat-mediafield__remove">×</button>';
var thumb = el.querySelector('.lat-media-card__thumb');
if (item.is_image) {
var img = document.createElement('img');
img.src = item.thumb;
img.alt = '';
thumb.appendChild(img);
} else {
var kind = document.createElement('span');
kind.className = 'lat-media-card__kind';
kind.textContent = item.kind;
thumb.appendChild(kind);
}
el.querySelector('.lat-media-card__name').textContent = item.name;
el.querySelector('.lat-media-card__name').title = item.name;
el.querySelector('.lat-mediafield__remove').addEventListener('click', function () {
el.remove();
write(ids().filter(function (id) { return id !== String(item.id); }));
});
return el;
}
function draw(items) {
tiles.innerHTML = '';
items.forEach(function (item) { tiles.appendChild(tile(item)); });
}
function load() {
var list = ids();
if (!list.length) { draw([]); return; }
fetch(root.getAttribute('data-resolve') + '?ids=' + encodeURIComponent(list.join(',')), { credentials: 'same-origin', headers: { 'X-Requested-With': 'fetch' } })
.then(function (r) { return r.ok ? r.json() : []; })
.then(draw)
.catch(function () { draw([]); });
}
function finish(items) {
var current = multiple ? ids() : [];
var merged = current.slice();
var existing = {};
tiles.querySelectorAll('[data-id]').forEach(function (t) { existing[t.getAttribute('data-id')] = true; });
items.forEach(function (item) {
if (merged.indexOf(String(item.id)) === -1) {
if (max > 1 && merged.length >= max) return;
merged.push(String(item.id));
}
if (!existing[String(item.id)]) tiles.appendChild(tile(item));
});
if (!multiple) {
tiles.innerHTML = '';
tiles.appendChild(tile(items[0]));
}
write(merged);
}
var picker = window.lat.mediaPicker({
multiple: multiple,
words: { title: words.title, use: words.use, cancel: words.cancel },
onDone: finish
});
pickBtn.addEventListener('click', function () {
var mode = root.getAttribute('data-mode');
picker.open(root.getAttribute('data-pick') + (mode && mode !== 'file' ? '?mode=' + mode : ''));
});
if (clearBtn) {
clearBtn.addEventListener('click', function () { draw([]); write([]); });
}
load();
return { ids: ids, set: write };
});
window.lat.widget('crop', function (root) {
var stage = root.querySelector('[data-crop-stage]');
var img = root.querySelector('[data-crop-image]');
var box = root.querySelector('[data-crop-box]');
var handle = root.querySelector('[data-crop-handle]');
var aspectSel = root.querySelector('[data-crop-aspect]');
var reset = root.querySelector('[data-crop-reset]');
var size = root.querySelector('[data-crop-size]');
var inputs = {
x: root.querySelector('[data-crop-x]'), y: root.querySelector('[data-crop-y]'),
w: root.querySelector('[data-crop-w]'), h: root.querySelector('[data-crop-h]'),
};
if (!stage || !img || !box) return;
var full = { w: parseInt(root.getAttribute('data-width') || '0', 10), h: parseInt(root.getAttribute('data-height') || '0', 10) };
var sel = { x: 0, y: 0, w: 0, h: 0 };
var aspect = 0;
function frame() {
var r = img.getBoundingClientRect();
var s = stage.getBoundingClientRect();
return { left: r.left - s.left, top: r.top - s.top, w: r.width, h: r.height };
}
function scale() {
var f = frame();
return f.w ? full.w / f.w : 1;
}
function clamp() {
var f = frame();
sel.w = Math.max(8, Math.min(sel.w, f.w));
sel.h = Math.max(8, Math.min(sel.h, f.h));
if (aspect) {
if (sel.w / sel.h > aspect) sel.w = sel.h * aspect; else sel.h = sel.w / aspect;
}
sel.x = Math.max(0, Math.min(sel.x, f.w - sel.w));
sel.y = Math.max(0, Math.min(sel.y, f.h - sel.h));
}
function draw() {
clamp();
var f = frame();
box.style.left = (f.left + sel.x) + 'px';
box.style.top = (f.top + sel.y) + 'px';
box.style.width = sel.w + 'px';
box.style.height = sel.h + 'px';
var k = scale();
var out = {
x: Math.round(sel.x * k), y: Math.round(sel.y * k),
w: Math.max(1, Math.round(sel.w * k)), h: Math.max(1, Math.round(sel.h * k)),
};
if (out.x + out.w > full.w) out.w = full.w - out.x;
if (out.y + out.h > full.h) out.h = full.h - out.y;
Object.keys(out).forEach(function (key) { if (inputs[key]) inputs[key].value = out[key]; });
if (size) size.textContent = out.w + ' \u00d7 ' + out.h;
window.lat.emit(root, 'crop:changed', out);
}
function whole() {
if (!full.w || !full.h) full = { w: img.naturalWidth, h: img.naturalHeight };
var f = frame();
sel = { x: 0, y: 0, w: f.w, h: f.h };
if (aspect) {
if (f.w / f.h > aspect) { sel.w = f.h * aspect; sel.x = (f.w - sel.w) / 2; }
else { sel.h = f.w / aspect; sel.y = (f.h - sel.h) / 2; }
}
draw();
}
var drag = null;
function point(e) {
var s = stage.getBoundingClientRect();
return { x: e.clientX - s.left, y: e.clientY - s.top };
}
box.addEventListener('pointerdown', function (e) {
var p = point(e);
var f = frame();
drag = e.target === handle
? { mode: 'size', px: p.x, py: p.y, w: sel.w, h: sel.h }
: { mode: 'move', px: p.x, py: p.y, x: sel.x, y: sel.y, f: f };
box.setPointerCapture(e.pointerId);
e.preventDefault();
});
box.addEventListener('pointermove', function (e) {
if (!drag) return;
var p = point(e);
if (drag.mode === 'move') {
sel.x = drag.x + (p.x - drag.px);
sel.y = drag.y + (p.y - drag.py);
} else {
sel.w = drag.w + (p.x - drag.px);
sel.h = drag.h + (p.y - drag.py);
}
draw();
});
['pointerup', 'pointercancel'].forEach(function (name) {
box.addEventListener(name, function () { drag = null; });
});
stage.addEventListener('pointerdown', function (e) {
if (e.target !== stage && e.target !== img) return;
var p = point(e);
var f = frame();
sel = { x: p.x - f.left, y: p.y - f.top, w: 8, h: 8 };
drag = { mode: 'size', px: p.x, py: p.y, w: 8, h: 8 };
box.setPointerCapture(e.pointerId);
draw();
e.preventDefault();
});
if (aspectSel) {
aspectSel.addEventListener('change', function () {
var v = aspectSel.value;
aspect = v === 'original' ? (full.h ? full.w / full.h : 0) : parseFloat(v) || 0;
whole();
});
}
if (reset) reset.addEventListener('click', whole);
var dialog = root.closest('dialog');
if (dialog) dialog.addEventListener('lat:dialog:opened', function () { setTimeout(whole, 0); });
if (img.complete) setTimeout(whole, 0); else img.addEventListener('load', whole);
window.addEventListener('resize', draw);
return { selection: function () { return sel; }, whole: whole };
});
(function () {
function closeAll(except) {
document.querySelectorAll('details.lat-menu[open]').forEach(function (m) {
if (m !== except && !m.hasAttribute('data-lat-keep-open')) m.open = false;
});
}
document.addEventListener('click', function (e) {
if (!e.target.closest) return;
var menu = e.target.closest('details.lat-menu');
var chose = menu && e.target.closest('.lat-menu__panel') && e.target.closest('a,button');
closeAll(chose ? null : menu);
});
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape') return;
var open = document.querySelector('details.lat-menu[open]');
if (!open || open.hasAttribute('data-lat-keep-open')) return;
open.open = false;
var summary = open.querySelector('summary');
if (summary) summary.focus();
});
})();
window.lat.widget('flash', function (el) {
var level = (el.className.match(/is-(success|error|info)/) || [])[1] || 'info';
window.lat.emit(el, 'flash:shown', { text: flashText(el), level: level });
if (el.classList.contains('is-error') || el.hasAttribute('data-lat-persist')) return;
setTimeout(function () { latDismissFlash(el); }, 5000);
});
window.lat.widget('repeater', function (root) {
var rows = root.querySelector('.lat-repeater__rows');
var blank = root.querySelector('.lat-repeater__blank');
var add = root.querySelector('.lat-repeater__add');
if (!rows || !blank || !add) return;
var reorder = root.getAttribute('data-lat-reorder') !== 'off';
var duplicate = root.getAttribute('data-lat-duplicate') !== 'off';
var untitled = blank.content.querySelector('.lat-repeater__untitled');
untitled = untitled ? untitled.textContent : 'Untitled';
function all() {
return Array.prototype.slice.call(rows.querySelectorAll(':scope > .lat-repeater__row'));
}
function renumber() {
var list = all();
list.forEach(function (row, index) {
row.querySelectorAll('[name]').forEach(function (control) {
control.name = control.name.replace(/\[[^\]]*\]/, '[' + index + ']');
});
var up = row.querySelector('.lat-repeater__up');
var down = row.querySelector('.lat-repeater__down');
if (up) up.disabled = index === 0;
if (down) down.disabled = index === list.length - 1;
});
}
var summaryIndex = parseInt(add.getAttribute('data-lat-summary-index'), 10) || 0;
function retitle(row) {
var title = row.querySelector('.lat-repeater__title');
if (!title) return;
var control = row.querySelectorAll('.lat-repeater__fields [name]')[summaryIndex];
var text = control ? String(control.value || '').split('\n')[0].trim() : '';
if (text) {
title.textContent = text;
} else {
title.textContent = '';
var span = document.createElement('span');
span.className = 'lat-repeater__untitled';
span.textContent = untitled;
title.appendChild(span);
}
}
function reveal(scope) {
scope.querySelectorAll('.lat-repeater__up, .lat-repeater__down, .lat-repeater__duplicate').forEach(function (b) {
b.hidden = false;
});
}
function fresh(after) {
var row = blank.content.cloneNode(true).firstElementChild;
if (after) after.insertAdjacentElement('afterend', row); else rows.appendChild(row);
renumber();
reveal(row);
window.lat.scan(row);
return row;
}
function addRow() {
if (!window.lat.emit(root, 'repeater:before-add', { count: all().length }, true)) return null;
var added = fresh(null);
var first = added.querySelector('.lat-repeater__fields [name], [name]');
if (first) first.focus();
var list = all();
window.lat.emit(root, 'repeater:added', { row: added, index: list.length - 1, count: list.length });
return added;
}
function removeRow(row) {
var index = all().indexOf(row);
if (index < 0) return false;
if (!window.lat.emit(root, 'repeater:before-remove', { row: row, index: index, count: all().length }, true)) {
return false;
}
row.remove();
renumber();
window.lat.emit(root, 'repeater:removed', { index: index, count: all().length });
return true;
}
function moveRow(from, to) {
var list = all();
var row = list[from];
var target = list[to];
if (!row || !target || from === to) return false;
if (to > from) target.insertAdjacentElement('afterend', row);
else target.insertAdjacentElement('beforebegin', row);
renumber();
var focus = row.querySelector(to > from ? '.lat-repeater__down' : '.lat-repeater__up');
if (focus && !focus.disabled) focus.focus();
window.lat.emit(root, 'repeater:moved', { row: row, from: from, to: to, count: list.length });
return true;
}
function copyValues(from, to) {
var sources = from.querySelectorAll('input, select, textarea');
var targets = to.querySelectorAll('input, select, textarea');
sources.forEach(function (source, i) {
var target = targets[i];
if (!target || target.tagName !== source.tagName || target.type !== source.type) return;
if (source.type === 'checkbox' || source.type === 'radio') target.checked = source.checked;
else target.value = source.value;
target.dispatchEvent(new Event('input', { bubbles: true }));
target.dispatchEvent(new Event('change', { bubbles: true }));
});
var pickers = from.querySelectorAll('[data-lat-widget="ref-picker"]');
to.querySelectorAll('[data-lat-widget="ref-picker"]').forEach(function (picker, i) {
var origin = pickers[i] && window.lat.get(pickers[i]);
var mine = window.lat.get(picker);
if (origin && mine && origin.value()) mine.choose({ id: origin.value(), label: origin.label() });
});
}
function duplicateRow(index) {
var list = all();
var source = list[index];
if (!source) return null;
if (!window.lat.emit(root, 'repeater:before-add', { count: list.length }, true)) return null;
var row = fresh(source);
copyValues(source, row);
if (row.tagName === 'DETAILS') {
retitle(row);
row.open = true;
}
var first = row.querySelector('.lat-repeater__fields [name], [name]');
if (first) first.focus();
window.lat.emit(root, 'repeater:duplicated', { row: row, index: index + 1, count: all().length });
return row;
}
add.addEventListener('click', addRow);
root.addEventListener('input', function (e) {
var row = e.target.closest('.lat-repeater__row');
if (row && row.tagName === 'DETAILS') retitle(row);
});
root.addEventListener('click', function (e) {
var button = e.target.closest('button');
var row = button && button.closest('.lat-repeater__row');
if (!row || !rows.contains(row)) return;
var index = all().indexOf(row);
if (button.classList.contains('lat-repeater__remove')) removeRow(row);
else if (reorder && button.classList.contains('lat-repeater__up')) moveRow(index, index - 1);
else if (reorder && button.classList.contains('lat-repeater__down')) moveRow(index, index + 1);
else if (duplicate && button.classList.contains('lat-repeater__duplicate')) duplicateRow(index);
else return;
e.preventDefault();
});
if (reorder || duplicate) reveal(rows);
renumber();
return {
add: addRow,
remove: function (index) {
var row = all()[index];
return row ? removeRow(row) : false;
},
move: moveRow,
duplicate: duplicateRow,
count: function () { return all().length; }
};
});
window.lat.widget('checklist', function (root) {
var pattern = root.getAttribute('data-lat-count') || '{n} / {total}';
var bar = root.querySelector('.lat-checklist__bar');
var search = root.querySelector('[data-lat-checklist-search]');
var total = root.querySelector('[data-lat-checklist-total]');
var empty = root.querySelector('.lat-checklist__empty');
function list(scope, selector) {
return Array.prototype.slice.call(scope.querySelectorAll(selector));
}
function boxes(scope) { return list(scope || root, '.lat-checklist__item input[type="checkbox"]'); }
function groups() { return list(root, '.lat-checklist__group'); }
function shown(box) { return !box.closest('.lat-checklist__item').hidden; }
function own(group, selector) {
var summary = group.firstElementChild;
return summary ? summary.querySelector(selector) : null;
}
function ticked(all) { return all.filter(function (b) { return b.checked; }); }
function values() { return ticked(boxes()).map(function (b) { return b.value; }); }
function count(n, of) { return pattern.replace('{n}', n).replace('{total}', of); }
function refresh() {
groups().forEach(function (group) {
var all = boxes(group);
var on = ticked(all).length;
var parent = own(group, '.lat-checklist__parent');
var label = own(group, '.lat-checklist__count');
if (parent) {
parent.checked = all.length > 0 && on === all.length;
parent.indeterminate = on > 0 && on < all.length;
parent.disabled = !all.some(function (b) { return !b.disabled; });
}
if (label) label.textContent = count(on, all.length);
});
if (total) total.textContent = count(ticked(boxes()).length, boxes().length);
}
function changed() {
refresh();
var on = values();
window.lat.emit(root, 'checklist:changed', { values: on, count: on.length, total: boxes().length });
}
function put(all, on) {
var moved = false;
all.forEach(function (box) {
if (box.disabled || box.checked === on) return;
box.checked = on;
moved = true;
});
return moved;
}
function filter(text) {
var query = (text || '').trim().toLowerCase();
boxes().forEach(function (box) {
var item = box.closest('.lat-checklist__item');
item.hidden = query !== '' && item.textContent.toLowerCase().indexOf(query) === -1;
});
var any = false;
groups().forEach(function (group) {
var matches = boxes(group).some(shown);
any = any || matches;
group.hidden = !matches;
if (query === '') {
if (group.hasAttribute('data-lat-was')) {
group.open = group.getAttribute('data-lat-was') === 'open';
group.removeAttribute('data-lat-was');
}
} else {
if (!group.hasAttribute('data-lat-was')) {
group.setAttribute('data-lat-was', group.open ? 'open' : 'closed');
}
group.open = matches;
}
});
any = any || boxes().some(shown);
if (empty) empty.hidden = any || boxes().length === 0;
}
root.addEventListener('change', function (e) {
var target = e.target;
if (target.classList.contains('lat-checklist__parent')) {
put(boxes(target.closest('.lat-checklist__group')).filter(shown), target.checked);
changed();
} else if (target.matches('.lat-checklist__item input[type="checkbox"]')) {
changed();
}
});
root.addEventListener('click', function (e) {
var all = e.target.closest('[data-lat-checklist-all]');
var none = e.target.closest('[data-lat-checklist-none]');
if (!all && !none) return;
if (put(boxes().filter(shown), !!all)) changed();
});
if (search) {
search.addEventListener('input', function () { filter(search.value); });
search.addEventListener('keydown', function (e) {
if (e.key !== 'Escape' || search.value === '') return;
e.stopPropagation();
search.value = '';
filter('');
});
}
list(root, '.lat-checklist__parent').forEach(function (parent) { parent.hidden = false; });
if (bar) bar.hidden = false;
refresh();
return {
values: values,
count: function () { return values().length; },
set: function (wanted) {
var moved = false;
boxes().forEach(function (box) {
var on = wanted.indexOf(box.value) !== -1;
if (box.disabled || box.checked === on) return;
box.checked = on;
moved = true;
});
if (moved) changed();
},
all: function () { if (put(boxes(), true)) changed(); },
none: function () { if (put(boxes(), false)) changed(); },
search: function (text) {
if (search) search.value = text || '';
filter(text);
}
};
});
window.lat.widget('pick-all', function (box) {
var table = box.closest('table');
if (!table) return;
box.addEventListener('change', function () {
table.querySelectorAll('tbody input[type="checkbox"][name="id"]').forEach(function (row) {
row.checked = box.checked;
});
latSelectionChanged(table);
});
});
document.addEventListener('click', function (e) {
var box = e.target;
if (!box.matches || !box.matches('tbody input[type="checkbox"][name="id"]')) return;
var table = box.closest('table');
if (!table) return;
var all = Array.prototype.slice.call(table.querySelectorAll('tbody input[type="checkbox"][name="id"]'));
var last = table._latLastPick;
var from = all.indexOf(last);
if (e.shiftKey && from !== -1 && last !== box) {
var to = all.indexOf(box);
all.slice(Math.min(from, to), Math.max(from, to) + 1).forEach(function (other) {
other.checked = box.checked;
});
}
table._latLastPick = box;
});
function latSelectionChanged(table) {
var ids = Array.prototype.map.call(
table.querySelectorAll('tbody input[type="checkbox"][name="id"]:checked'),
function (box) { return box.value; }
);
window.lat.emit(table, 'selection:changed', { ids: ids, count: ids.length });
}
document.addEventListener('change', function (e) {
if (!e.target.matches || !e.target.matches('tbody input[type="checkbox"][name="id"]')) return;
var table = e.target.closest('table');
if (table) latSelectionChanged(table);
});
window.lat.widget('bulk-delete', function (btn) {
function sync() {
btn.disabled = !document.querySelector('input[type="checkbox"][name="id"]:checked');
}
document.addEventListener('change', function (e) {
if (e.target.matches('input[name="id"], [data-lat-widget="pick-all"]')) sync();
});
document.addEventListener('htmx:load', sync);
sync();
});
window.lat.widget('copy', function (btn) {
var label = btn.textContent;
btn.addEventListener('click', function () {
var group = btn.closest('.lat-input-group');
var input = group && group.querySelector('input');
if (!input || !navigator.clipboard) return;
navigator.clipboard.writeText(input.value).then(function () {
window.lat.emit(btn, 'copy:copied', { value: input.value });
btn.classList.add('is-copied');
btn.textContent = 'Copied';
setTimeout(function () {
btn.classList.remove('is-copied');
btn.textContent = label;
}, 1200);
});
});
});
(function () {
var NOT_TEXT = /^(checkbox|radio|file|button|submit|reset|image|range|color)$/;
function isSingleLine(el) {
return el.tagName === 'INPUT' && !NOT_TEXT.test(el.type);
}
function rule(el) {
var carrier = el.closest('[data-lat-enter]');
return carrier ? carrier.getAttribute('data-lat-enter') : 'submit';
}
function focusables(root) {
return Array.prototype.filter.call(
root.querySelectorAll('input, select, textarea, button, [tabindex]'),
function (f) { return !f.disabled && f.type !== 'hidden' && f.offsetParent !== null; }
);
}
function focusNext(el, root) {
var list = focusables(root);
var i = list.indexOf(el);
if (i < 0 || i + 1 >= list.length) return false;
list[i + 1].focus();
return true;
}
function withinRows(el, scope) {
var row = el.closest('.lat-repeater__row');
if (row && focusNext(el, row)) return;
var add = scope.querySelector(':scope > .lat-repeater__add');
if (add) add.click();
}
document.addEventListener('keydown', function (e) {
if (e.key !== 'Enter' || e.isComposing || e.defaultPrevented) return;
var el = e.target;
if (!el || !el.closest) return;
var form = el.form || el.closest('form');
if (e.metaKey || e.ctrlKey) {
if (!form) return;
e.preventDefault();
form.requestSubmit();
return;
}
if (!isSingleLine(el)) return;
var scope = el.closest('[data-lat-enter-scope]');
if (scope && form && form.contains(scope)) {
e.preventDefault();
var kind = scope.getAttribute('data-lat-enter-scope');
var target = scope.getAttribute('data-lat-enter-target');
if (kind === 'rows') withinRows(el, scope);
else if (target) { var t = scope.querySelector(target); if (t) t.click(); }
return;
}
var what = rule(el);
if (what === 'off') e.preventDefault();
else if (what === 'next') { e.preventDefault(); focusNext(el, form || document); }
}, true);
})();
(function () {
var guarded = [];
var leaving = false;
function snapshot(form) {
var parts = [];
new FormData(form).forEach(function (value, key) {
if (key === '_csrf') return;
parts.push(key + '=' + (typeof value === 'string' ? value : value.name));
});
return parts.join('&');
}
function changed() {
return guarded.some(function (g) {
return document.body.contains(g.form) && !g.sent && (g.refused || snapshot(g.form) !== g.opened);
});
}
window.lat.widget('form', function (form) {
var refused = form.hasAttribute('data-lat-refused');
var state = { form: form, opened: snapshot(form), refused: refused, sent: false };
if (form.getAttribute('data-lat-confirm-leave') !== 'off') {
guarded = guarded.filter(function (g) { return document.body.contains(g.form); });
guarded.push(state);
}
form.addEventListener('submit', function () { state.sent = true; });
form.addEventListener('htmx:sendError', function () { state.sent = false; });
form.addEventListener('htmx:responseError', function () { state.sent = false; });
var focus = form.getAttribute('data-lat-focus');
if (focus !== 'off') {
var field = refused ? form.querySelector('.lat-field__error') : null;
var scope = field ? field.closest('.lat-field') : (refused || focus === 'first' ? form : null);
var control = scope && scope.querySelector(
'input:not([type="hidden"]):not([disabled]):not([readonly]), select:not([disabled]), textarea:not([disabled]):not([readonly])'
);
if (control) {
control.focus({ preventScroll: !refused });
if (refused && control.scrollIntoView) control.scrollIntoView({ block: 'center' });
}
}
return {
changed: function () { return !state.sent && (state.refused || snapshot(form) !== state.opened); },
settle: function () { state.refused = false; state.opened = snapshot(form); }
};
});
window.addEventListener('beforeunload', function (e) {
if (leaving || !changed()) return;
e.preventDefault();
e.returnValue = '';
});
document.addEventListener('click', function (e) {
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
var link = e.target.closest ? e.target.closest('a[href]') : null;
if (!link || link.target === '_blank' || link.hasAttribute('download')) return;
var href = link.getAttribute('href');
if (!href || href.charAt(0) === '#' || /^javascript:/i.test(href)) return;
if (!changed()) return;
e.preventDefault();
var body = document.body;
window.lat.confirm(
body.getAttribute('data-lat-leave') || 'You have unsaved changes. Leave without saving?',
{
go: body.getAttribute('data-lat-leave-go') || 'Leave',
cancel: body.getAttribute('data-lat-leave-stay') || 'Stay',
focus: 'cancel'
},
function (yes) {
if (!yes) return;
leaving = true;
window.location.href = link.href;
}
);
});
})();
(function () {
function counter(control) {
var next = control.closest('.lat-input-group') || control;
var el = next.nextElementSibling;
if (el && el.classList.contains('lat-counter')) return el;
el = document.createElement('p');
el.className = 'lat-counter';
el.setAttribute('aria-live', 'polite');
next.parentNode.insertBefore(el, next.nextSibling);
return el;
}
function update(control) {
var max = parseInt(control.getAttribute('maxlength'), 10);
if (!max) return;
var used = control.value.length;
var el = counter(control);
el.textContent = used + ' / ' + max;
el.classList.toggle('is-full', used >= max);
el.hidden = control.getAttribute('data-lat-counter') === 'auto' && used < max * 0.8;
}
function all(root) {
var scope = root && root.querySelectorAll ? root : document;
scope.querySelectorAll('[data-lat-counter]').forEach(update);
}
document.addEventListener('input', function (e) {
if (e.target.hasAttribute && e.target.hasAttribute('data-lat-counter')) update(e.target);
});
document.addEventListener('DOMContentLoaded', function () { all(document); });
document.addEventListener('htmx:load', function (e) { all(e.target); });
})();
(function () {
if (window.CSS && CSS.supports && CSS.supports('field-sizing', 'content')) return;
function fit(area) {
if (area.getAttribute('data-lat-grow') === 'off') return;
area.style.height = 'auto';
area.style.height = (area.scrollHeight + 2) + 'px';
}
function all(root) {
var scope = root && root.querySelectorAll ? root : document;
scope.querySelectorAll('textarea.lat-input').forEach(fit);
}
document.addEventListener('input', function (e) {
if (e.target.matches && e.target.matches('textarea.lat-input')) fit(e.target);
});
document.addEventListener('DOMContentLoaded', function () { all(document); });
document.addEventListener('htmx:load', function (e) { all(e.target); });
})();
window.lat.widget('reveal', function (btn) {
var group = btn.closest('.lat-input-group');
var input = group && group.querySelector('input');
if (!input) return;
function show(on) {
input.type = on ? 'text' : 'password';
btn.setAttribute('aria-pressed', on ? 'true' : 'false');
window.lat.emit(btn, on ? 'reveal:shown' : 'reveal:hidden', {});
}
btn.hidden = false;
btn.addEventListener('click', function () { show(input.type === 'password'); });
if (input.form) {
input.form.addEventListener('submit', function () {
if (input.type !== 'password') show(false);
});
}
return { show: function () { show(true); }, hide: function () { show(false); } };
});
(function () {
var LETTERS = { 'ß': 'ss', 'æ': 'ae', 'ø': 'o', 'œ': 'oe', 'đ': 'd', 'ł': 'l', 'þ': 'th' };
function slug(text) {
return text
.toLowerCase()
.replace(/[ßæøœđłþ]/g, function (c) { return LETTERS[c]; })
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
function shape(text, kind) {
if (text.trim() === '') return '';
if (kind === 'exact') return text;
if (kind === 'url') return '/' + slug(text);
if (kind === 'file') return text.trim().replace(/\s+/g, '-');
return slug(text);
}
function control(wrapper) {
return wrapper.querySelector('input:not([type="hidden"]), textarea');
}
function followers(form, name) {
return Array.prototype.filter.call(form.querySelectorAll('[data-lat-preset]'), function (w) {
return w.getAttribute('data-lat-preset') === name;
});
}
document.addEventListener('input', function (e) {
var source = e.target;
var form = source.form;
if (!form || !source.name) return;
var own = source.closest('[data-lat-preset]');
if (own && control(own) === source && !source._latPreset) {
own._latTaken = source.value !== '';
}
followers(form, source.name).forEach(function (wrapper) {
var target = control(wrapper);
if (!target || wrapper._latTaken) return;
var value = shape(source.value, wrapper.getAttribute('data-lat-preset-type'));
if (target.value === value) return;
target.value = value;
target._latPreset = true;
target.dispatchEvent(new Event('input', { bubbles: true }));
target._latPreset = false;
window.lat.emit(target, 'preset:filled', { from: source.name, value: value });
});
});
function settle(root) {
var scope = root && root.querySelectorAll ? root : document;
scope.querySelectorAll('[data-lat-preset]').forEach(function (wrapper) {
var target = control(wrapper);
if (target && target.value !== '') wrapper._latTaken = true;
});
}
document.addEventListener('DOMContentLoaded', function () { settle(document); });
document.addEventListener('htmx:load', function (e) { settle(e.target); });
})();
(function () {
function values(form, field) {
var many = /\[\]$/.test(field);
var name = field.replace(/\[\]$/, '');
var out = [];
var controls = form.querySelectorAll('[name]');
Array.prototype.forEach.call(controls, function (c) {
var mine = many ? (c.name === name || c.name.indexOf(name + '[') === 0) : c.name === name;
if (!mine) return;
if (c.type === 'checkbox' || c.type === 'radio') {
if (c.checked) out.push(c.value);
} else if (c.tagName === 'SELECT' && c.multiple) {
Array.prototype.forEach.call(c.selectedOptions, function (o) { out.push(o.value); });
} else {
out.push(c.value);
}
});
return out;
}
function ticked(form, field) {
var name = field.replace(/\[\]$/, '');
return Array.prototype.some.call(form.querySelectorAll('[name]'), function (c) {
return (c.type === 'checkbox' || c.type === 'radio') && (c.name === name || c.name.indexOf(name + '[') === 0) && c.checked;
});
}
function wanted(condition) {
var list = [];
var re = /\[([^\]]*)\]/g, m;
while ((m = re.exec(condition))) list.push(m[1]);
return list;
}
function matches(want, have) {
if (want === '*') return have !== '';
if (want.indexOf('*') === -1) return want === have;
var head = want.split('*')[0], tail = want.split('*').slice(1).join('*');
return have.indexOf(head) === 0 && (tail === '' || have.slice(-tail.length) === tail);
}
function met(form, wrapper) {
var field = wrapper.getAttribute('data-lat-trigger-field');
var condition = wrapper.getAttribute('data-lat-trigger-condition');
if (condition === 'checked') return ticked(form, field);
if (condition === 'unchecked') return !ticked(form, field);
var have = values(form, field);
var want = wanted(condition);
if (want.length === 0 || (want.length === 1 && want[0] === '')) return have.every(function (v) { return v === ''; });
return have.some(function (v) { return want.some(function (w) { return matches(w, v); }); });
}
function conceal(wrapper, controls, hidden) {
wrapper.hidden = hidden;
controls.forEach(function (c) {
if (hidden && !c.disabled) { c.disabled = true; c._latConcealed = true; }
else if (!hidden && c._latConcealed) { c.disabled = false; c._latConcealed = false; }
});
}
function apply(form, wrapper) {
var on = met(form, wrapper);
var controls = wrapper.querySelectorAll('input:not([type="hidden"]), select, textarea, button');
wrapper.getAttribute('data-lat-trigger-action').split('|').forEach(function (action) {
if (action === 'show') conceal(wrapper, controls, !on);
else if (action === 'hide') conceal(wrapper, controls, on);
else if (action === 'enable') controls.forEach(function (c) { c.disabled = !on; });
else if (action === 'disable') controls.forEach(function (c) { c.disabled = on; });
else if (on && action === 'empty') controls.forEach(function (c) {
if (c.type === 'checkbox' || c.type === 'radio') c.checked = false;
else if (c.tagName !== 'BUTTON') c.value = '';
});
else if (on && action.indexOf('fill[') === 0) {
var value = action.slice(5, -1);
controls.forEach(function (c) {
if (c.type === 'checkbox' || c.type === 'radio') c.checked = c.value === value;
else if (c.tagName !== 'BUTTON') c.value = value;
});
}
});
if (wrapper._latMet !== on) {
wrapper._latMet = on;
window.lat.emit(wrapper, 'trigger:changed', { field: wrapper.getAttribute('data-lat-trigger-field'), met: on });
}
}
function all(form) {
form.querySelectorAll('[data-lat-trigger-field]').forEach(function (w) { apply(form, w); });
}
function watching(e) {
var form = e.target.form || (e.target.closest && e.target.closest('form'));
if (form && form.querySelector('[data-lat-trigger-field]')) all(form);
}
document.addEventListener('change', watching);
document.addEventListener('input', watching);
function start(root) {
var scope = root && root.querySelectorAll ? root : document;
var forms = Array.prototype.slice.call(scope.querySelectorAll('form'));
if (scope.matches && scope.matches('form')) forms.push(scope);
forms.forEach(function (form) {
if (form.querySelector('[data-lat-trigger-field]')) all(form);
});
}
document.addEventListener('DOMContentLoaded', function () { start(document); });
document.addEventListener('htmx:load', function (e) { start(e.target); });
})();
(function () {
function target(e) {
var row = e.target.closest ? e.target.closest('tr[data-lat-href]') : null;
if (!row) return null;
if (e.target.closest('a, button, input, select, textarea, label, form, [data-lat-widget]')) return null;
return row;
}
function open(url, newTab) {
if (newTab) window.open(url, '_blank', 'noopener');
else window.location.href = url;
}
document.addEventListener('click', function (e) {
if (e.defaultPrevented || e.button !== 0) return;
var row = target(e);
if (!row) return;
var selection = window.getSelection ? window.getSelection() : null;
if (selection && String(selection) !== '' && row.contains(selection.anchorNode)) return;
open(row.getAttribute('data-lat-href'), e.metaKey || e.ctrlKey);
});
document.addEventListener('auxclick', function (e) {
if (e.button !== 1) return;
var row = target(e);
if (!row) return;
e.preventDefault();
open(row.getAttribute('data-lat-href'), true);
});
document.addEventListener('keydown', function (e) {
var row = e.target;
if (e.key !== 'Enter' || !row.matches || !row.matches('tr[data-lat-href]')) return;
e.preventDefault();
open(row.getAttribute('data-lat-href'), e.metaKey || e.ctrlKey);
});
})();
(function () {
var pending = null;
window.lat.widget('reorder', function (th) {
var table = th.closest('table');
var path = th.getAttribute('data-reorder-path');
var region = th.getAttribute('data-reorder-region');
if (!table || !path || !window.htmx) return;
var dragging = null;
function rows() { return Array.prototype.slice.call(table.querySelectorAll('tbody tr[data-lat-id]')); }
function clear() {
rows().forEach(function (r) { r.classList.remove('is-drop-before', 'is-drop-after'); });
}
function siblings(row) {
var parent = row.getAttribute('data-lat-parent');
if (!parent) return null;
var parentRow = table.querySelector('tr[data-lat-id="' + parent + '"]');
if (!parentRow) return null;
var depth = +parentRow.getAttribute('data-lat-depth') || 0;
var next = parentRow.nextElementSibling;
while (next && (+next.getAttribute('data-lat-depth') || 0) > depth) {
var gone = next;
next = next.nextElementSibling;
gone.remove();
}
return parentRow;
}
function post(values, row) {
values._csrf = th.getAttribute('data-reorder-csrf');
values.page = th.getAttribute('data-reorder-page');
values.parent = row.getAttribute('data-lat-parent') || '';
values.depth = row.getAttribute('data-lat-depth') || '';
var parentRow = siblings(row);
if (parentRow) {
window.htmx.ajax('POST', path, { target: parentRow, swap: 'afterend', values: values });
} else {
window.htmx.ajax('POST', path, { target: '#' + region, swap: 'outerHTML', values: values });
}
}
table.addEventListener('dragstart', function (e) {
var grip = e.target.closest ? e.target.closest('.lat-grip') : null;
var row = grip && grip.closest('tr[data-lat-id]');
if (!row) return;
dragging = row;
row.classList.add('is-dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', row.getAttribute('data-lat-id'));
});
table.addEventListener('dragover', function (e) {
if (!dragging) return;
var row = e.target.closest ? e.target.closest('tr[data-lat-id]') : null;
if (!row || row === dragging) return;
e.preventDefault();
var box = row.getBoundingClientRect();
var before = e.clientY < box.top + box.height / 2;
clear();
row.classList.add(before ? 'is-drop-before' : 'is-drop-after');
});
table.addEventListener('drop', function (e) {
if (!dragging) return;
var row = e.target.closest ? e.target.closest('tr[data-lat-id]') : null;
e.preventDefault();
if (row && row !== dragging && row.getAttribute('data-lat-parent') === dragging.getAttribute('data-lat-parent')) {
var before = row.classList.contains('is-drop-before');
var values = { id: dragging.getAttribute('data-lat-id') };
values[before ? 'before' : 'after'] = row.getAttribute('data-lat-id');
post(values, dragging);
}
clear();
});
table.addEventListener('dragend', function () {
if (dragging) dragging.classList.remove('is-dragging');
dragging = null;
clear();
});
table.addEventListener('submit', function (e) {
var form = e.target;
if (!form.classList.contains('lat-grip__moves')) return;
var button = form.querySelector('.lat-grip__move:focus') || e.submitter;
var row = form.closest('tr[data-lat-id]');
if (button && row) pending = { id: row.getAttribute('data-lat-id'), move: button.value };
if (row && row.getAttribute('data-lat-parent') && button) {
e.preventDefault();
post({ id: row.getAttribute('data-lat-id'), move: button.value }, row);
}
});
if (pending) {
var row = table.querySelector('tr[data-lat-id="' + pending.id + '"]');
var button = row && row.querySelector('.lat-grip__move[value="' + pending.move + '"]');
pending = null;
if (button) (button.disabled ? button.form.querySelector('.lat-grip__move:not(:disabled)') : button).focus();
}
});
})();
(function () {
window.lat.widget('tree', function (th) {
var table = th.closest('table');
if (!table || !window.htmx) return;
table.addEventListener('click', function (e) {
var toggle = e.target.closest ? e.target.closest('.lat-tree__toggle') : null;
if (!toggle || !table.contains(toggle)) return;
e.preventDefault();
var row = toggle.closest('tr[data-lat-id]');
if (!row) return;
var depth = +row.getAttribute('data-lat-depth') || 0;
if (toggle.getAttribute('aria-expanded') === 'true') {
var next = row.nextElementSibling;
while (next && (+next.getAttribute('data-lat-depth') || 0) > depth) {
var gone = next;
next = next.nextElementSibling;
gone.remove();
}
toggle.setAttribute('aria-expanded', 'false');
toggle.classList.remove('is-expanded');
return;
}
toggle.setAttribute('aria-expanded', 'true');
toggle.classList.add('is-expanded');
window.htmx.ajax('GET', toggle.getAttribute('href'), { source: toggle, target: row, swap: 'afterend' });
});
});
})();
window.lat.widget('cell', function () {});
window.lat.widget('menu-order', function (nav) {
var post = nav.getAttribute('data-menu-post');
if (!post) return;
var dragging = null;
function tabs() { return Array.prototype.slice.call(nav.querySelectorAll('[data-lat-key]')); }
function clear() { tabs().forEach(function (t) { t.classList.remove('is-drop-before', 'is-drop-after'); }); }
nav.addEventListener('dragstart', function (e) {
var tab = e.target.closest ? e.target.closest('[draggable="true"]') : null;
if (!tab) return;
dragging = tab;
tab.classList.add('is-dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', tab.getAttribute('data-lat-key'));
});
nav.addEventListener('dragover', function (e) {
if (!dragging) return;
var tab = e.target.closest ? e.target.closest('[draggable="true"]') : null;
if (!tab || tab === dragging) return;
e.preventDefault();
var box = tab.getBoundingClientRect();
clear();
var before = nav.hasAttribute('data-menu-vertical')
? e.clientY < box.top + box.height / 2
: e.clientX < box.left + box.width / 2;
tab.classList.add(before ? 'is-drop-before' : 'is-drop-after');
});
nav.addEventListener('drop', function (e) {
if (!dragging) return;
var tab = e.target.closest ? e.target.closest('[draggable="true"]') : null;
e.preventDefault();
if (tab && tab !== dragging) {
tab.insertAdjacentElement(tab.classList.contains('is-drop-before') ? 'beforebegin' : 'afterend', dragging);
var order = tabs().map(function (t) { return t.getAttribute('data-lat-key'); }).join(',');
var body = new URLSearchParams();
body.set('_csrf', nav.getAttribute('data-menu-csrf') || '');
body.set('order', order);
if (nav.getAttribute('data-menu-hierarchy')) body.set('hierarchy', nav.getAttribute('data-menu-hierarchy'));
fetch(post, { method: 'POST', body: body, credentials: 'same-origin', headers: { 'X-Requested-With': 'fetch' } })
.catch(function () { window.lat.flash && window.lat.flash('The menu order could not be saved.', 'error'); });
}
clear();
});
nav.addEventListener('dragend', function () {
if (dragging) dragging.classList.remove('is-dragging');
dragging = null;
clear();
});
});
window.lat.widget('palette', function (button) {
var url = button.getAttribute('data-palette-url');
if (!url) return;
var overlay = document.createElement('div');
overlay.className = 'lat-palette';
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.innerHTML =
'<div class="lat-palette__backdrop" data-palette-close></div>' +
'<div class="lat-palette__panel">' +
'<input class="lat-palette__input" type="text" autocomplete="off" role="combobox" aria-expanded="true" aria-autocomplete="list" aria-label="' + (button.getAttribute('data-palette-placeholder') || 'Go to').replace(/"/g, '"') + '">' +
'<ul class="lat-palette__list" role="listbox"></ul>' +
'</div>';
document.body.appendChild(overlay);
var input = overlay.querySelector('input');
var list = overlay.querySelector('ul');
input.placeholder = button.getAttribute('data-palette-placeholder') || '';
var active = -1;
var timer = null;
var latest = 0;
function items() { return Array.prototype.slice.call(list.querySelectorAll('.lat-palette__hit')); }
function mark(index) {
var all = items();
if (!all.length) { active = -1; return; }
active = (index + all.length) % all.length;
all.forEach(function (a, i) { a.classList.toggle('is-active', i === active); });
all[active].scrollIntoView({ block: 'nearest' });
}
function show(found) {
list.innerHTML = '';
if (!found.length) {
var empty = document.createElement('li');
empty.className = 'lat-palette__empty';
empty.textContent = button.getAttribute('data-palette-empty') || '';
list.appendChild(empty);
active = -1;
return;
}
found.forEach(function (hit) {
var li = document.createElement('li');
var a = document.createElement('a');
a.className = 'lat-palette__hit';
a.href = hit.href;
a.setAttribute('role', 'option');
var label = document.createElement('span');
label.className = 'lat-palette__label';
label.textContent = hit.label;
a.appendChild(label);
if (hit.hint) {
var hint = document.createElement('span');
hint.className = 'lat-palette__hint';
hint.textContent = hit.hint;
a.appendChild(hint);
}
li.appendChild(a);
list.appendChild(li);
});
mark(0);
}
function search() {
var seq = ++latest;
fetch(url + '?q=' + encodeURIComponent(input.value), { credentials: 'same-origin', headers: { 'X-Requested-With': 'fetch' } })
.then(function (r) { return r.json(); })
.then(function (data) { if (seq === latest) show(data.items || []); })
.catch(function () { if (seq === latest) show([]); });
}
function open() {
overlay.classList.add('is-open');
input.value = '';
input.focus();
search();
}
function close() {
overlay.classList.remove('is-open');
button.focus();
}
button.addEventListener('click', function (e) { e.preventDefault(); open(); });
overlay.addEventListener('click', function (e) {
if (e.target.closest('[data-palette-close]')) close();
});
input.addEventListener('input', function () {
clearTimeout(timer);
timer = setTimeout(search, 150);
});
input.addEventListener('keydown', function (e) {
if (e.key === 'ArrowDown') { e.preventDefault(); mark(active + 1); }
else if (e.key === 'ArrowUp') { e.preventDefault(); mark(active - 1); }
else if (e.key === 'Enter') {
var all = items();
if (active >= 0 && all[active]) { e.preventDefault(); window.location.href = all[active].href; }
} else if (e.key === 'Escape') { e.preventDefault(); close(); }
});
document.addEventListener('keydown', function (e) {
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey && (e.key === 'k' || e.key === 'K')) {
e.preventDefault();
if (overlay.classList.contains('is-open')) close(); else open();
}
});
});
(function () {
function controls(bar) {
return Array.prototype.filter.call(bar.querySelectorAll('button, select, a[href], input'), function (c) {
return !c.disabled && !c.hidden && c.offsetParent !== null;
});
}
function arm(bar) {
if (bar.getAttribute('data-lat-toolbar') === '1') return;
bar.setAttribute('data-lat-toolbar', '1');
var items = controls(bar);
items.forEach(function (c, i) { c.setAttribute('tabindex', i === 0 ? '0' : '-1'); });
bar.addEventListener('focusin', function (e) {
controls(bar).forEach(function (c) { c.setAttribute('tabindex', c === e.target ? '0' : '-1'); });
});
bar.addEventListener('keydown', function (e) {
if (['ArrowRight', 'ArrowLeft', 'Home', 'End'].indexOf(e.key) === -1) return;
if (e.target.tagName === 'SELECT' && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) return;
var list = controls(bar);
var at = list.indexOf(e.target);
if (at === -1) return;
e.preventDefault();
var next = e.key === 'Home' ? 0 : e.key === 'End' ? list.length - 1
: e.key === 'ArrowRight' ? (at + 1) % list.length : (at - 1 + list.length) % list.length;
list[next].focus();
});
}
function all(root) {
var scope = root && root.querySelectorAll ? root : document;
scope.querySelectorAll('[role="toolbar"]').forEach(arm);
}
document.addEventListener('DOMContentLoaded', function () { all(document); });
document.addEventListener('htmx:load', function (e) { all(e.target); });
document.addEventListener('lat:richeditor:ready', function (e) { all(e.target); });
document.addEventListener('lat:markdown:ready', function (e) { all(e.target); });
})();
window.lat.widget('dashlist', function (form) {
form.addEventListener('click', function (e) {
var btn = e.target.closest ? e.target.closest('[data-move]') : null;
if (!btn) return;
var row = btn.closest('.lat-dashlist__row');
var list = row && row.parentNode;
if (!list) return;
if (btn.getAttribute('data-move') === 'up' && row.previousElementSibling) {
list.insertBefore(row, row.previousElementSibling);
} else if (btn.getAttribute('data-move') === 'down' && row.nextElementSibling) {
list.insertBefore(row.nextElementSibling, row);
}
btn.focus();
});
});
window.lat.widget('aside', function (root) {
var fold = root.querySelector('.lat-aside__fold');
if (!fold || !window.matchMedia) return;
var small = window.matchMedia('(max-width: 992px)');
function apply() { fold.open = !small.matches; }
apply();
if (small.addEventListener) small.addEventListener('change', apply);
else if (small.addListener) small.addListener(apply);
});
window.lat.widget('sensitive', function (root) {
var input = root.querySelector('input');
var btn = root.querySelector('.lat-sensitive__btn');
if (!input || !btn) return;
if (!root.hasAttribute('data-has-value')) { input.type = 'text'; return; }
btn.hidden = false;
var shown = false;
function hide() {
shown = false;
input.type = 'password';
btn.setAttribute('aria-pressed', 'false');
}
btn.addEventListener('click', function () {
if (shown) { hide(); return; }
var url = new URL('reveal/' + encodeURIComponent(root.getAttribute('data-field')), location.href.replace(/\/edit\/?(\?.*)?$/, '/'));
fetch(url, { credentials: 'same-origin', headers: { 'X-Requested-With': 'fetch' } })
.then(function (r) { return r.ok ? r.text() : Promise.reject(r.status); })
.then(function (text) {
input.value = text;
input.type = 'text';
shown = true;
btn.setAttribute('aria-pressed', 'true');
window.lat.emit(root, 'sensitive:revealed', { field: root.getAttribute('data-field') });
})
.catch(function () { window.lat.flash(root.getAttribute('data-failed') || 'Could not reveal the value.', 'error'); });
});
input.addEventListener('input', function () { if (input.type === 'password') input.type = 'text'; });
if (input.form) input.form.addEventListener('submit', function () { if (shown) hide(); });
return { hide: hide };
});