(function() {
'use strict';
const CONFIG = {
formSelector: 'form[w-boost], form[action^="/w-action"]',
cacheTimeout: 300000, loadingClass: 'w-loading',
activeClass: 'active'
};
const pageCache = new Map();
const wArmedTriggers = new WeakSet();
const wFetchLifecycles = [];
const _debugMeta = document.querySelector('meta[name="what-debug"]');
const DEBUG_LEVEL = _debugMeta ? _debugMeta.getAttribute('content') : 'off';
let DEBUG = DEBUG_LEVEL !== 'off';
const _levels = { off: -1, error: 0, warn: 1, info: 2, verbose: 3, debug: 3 };
const _currentLevel = _levels[DEBUG_LEVEL] !== undefined ? _levels[DEBUG_LEVEL] : -1;
function debug(...args) {
if (!DEBUG) return;
let level = 'info';
if (args.length > 1 && typeof args[0] === 'string' && _levels[args[0]] !== undefined) {
level = args.shift();
}
if ((_levels[level] || 0) <= _currentLevel) {
const method = level === 'error' ? 'error' : level === 'warn' ? 'warn' : 'log';
console[method]('[What]', ...args);
}
}
function getCsrfToken() {
const meta = document.querySelector('meta[name="csrf-token"]');
return meta ? meta.getAttribute('content') : null;
}
function addCsrfHeader(headers) {
const token = getCsrfToken();
if (token) {
headers['X-CSRF-Token'] = token;
}
return headers;
}
function init() {
document.addEventListener('click', handleLinkClick);
document.addEventListener('submit', handleFormSubmit);
document.addEventListener('click', handleTriggerClick);
document.addEventListener('change', handleTriggerChange);
document.addEventListener('click', handlePartialFetch);
document.addEventListener('click', handleWSet);
document.addEventListener('input', handleWSetInput);
document.addEventListener('click', handleClipboardClick);
document.addEventListener('click', handleThemeToggle);
document.addEventListener('click', handleModalClick);
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
window.addEventListener('popstate', handlePopState);
initializeElements();
initFormValidation();
initWire();
console.log('[What] Initialized');
}
function initializeElements() {
const autofocus = document.querySelector('[w-autofocus]');
if (autofocus) autofocus.focus();
sweepFetchLifecycles();
initFetchTriggers();
}
function handleModalClick(event) {
const trigger = event.target.closest('[w-modal-trigger]');
if (trigger) {
event.preventDefault();
const modal = document.getElementById(trigger.getAttribute('w-modal-trigger'));
if (modal) toggleModal(modal, true);
return;
}
const close = event.target.closest('[w-modal-close]');
if (close) {
event.preventDefault();
const modal = close.closest('.modal-backdrop') || close.closest('.drawer-backdrop');
if (modal) toggleModal(modal, false);
}
}
const wCopiedTimers = new WeakMap();
async function handleClipboardClick(event) {
const el = event.target.closest('[w-clipboard], [w-clipboard-from]');
if (!el) return;
event.preventDefault();
let text = el.getAttribute('w-clipboard');
if (text === null) {
const selector = el.getAttribute('w-clipboard-from');
const source = document.querySelector(selector);
if (!source) {
debug('warn', 'w-clipboard-from target not found:', selector);
return;
}
if (source.tagName === 'A') text = source.href;
else if (source.tagName === 'INPUT' || source.tagName === 'TEXTAREA' || source.tagName === 'SELECT') text = source.value;
else text = source.textContent.trim();
}
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
} else {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
textarea.remove();
}
} catch (e) {
debug('error', 'clipboard copy failed:', e);
return;
}
const prior = wCopiedTimers.get(el);
if (prior) clearTimeout(prior.timer);
const originalHtml = prior ? prior.html : el.innerHTML;
el.classList.add('w-copied');
const copiedLabel = el.getAttribute('w-copied-label');
if (copiedLabel) el.textContent = copiedLabel;
const timer = setTimeout(function() {
el.classList.remove('w-copied');
if (copiedLabel) el.innerHTML = originalHtml;
wCopiedTimers.delete(el);
}, 1500);
wCopiedTimers.set(el, { timer: timer, html: originalHtml });
}
function handleThemeToggle(event) {
const el = event.target.closest('[w-theme-toggle]');
if (!el) return;
event.preventDefault();
const root = document.documentElement.classList;
const isDark = root.contains('dark') ||
(!root.contains('light') && window.matchMedia('(prefers-color-scheme: dark)').matches);
const next = isDark ? 'light' : 'dark';
root.remove('dark', 'light');
root.add(next);
try { localStorage.setItem('w-theme', next); } catch (e) { }
document.dispatchEvent(new CustomEvent('w:theme', { detail: { theme: next } }));
}
function handleLinkClick(event) {
const link = event.target.closest('a');
if (!link) return;
if (
link.hostname !== window.location.hostname ||
link.hasAttribute('target') ||
link.hasAttribute('download') ||
link.hasAttribute('w-modal-trigger') ||
link.hasAttribute('w-clipboard') ||
link.hasAttribute('w-clipboard-from') ||
link.hasAttribute('w-theme-toggle') ||
event.metaKey || event.ctrlKey ||
link.getAttribute('w-boost') === 'false'
) {
return;
}
const href = link.getAttribute('href');
if (!href || href.startsWith('#') || href.startsWith('javascript:')) {
return;
}
const linkConfirm = link.getAttribute('w-confirm');
if (linkConfirm && !window.confirm(linkConfirm)) {
event.preventDefault();
return;
}
event.preventDefault();
debug('boost →', href);
navigateTo(href);
}
async function navigateTo(url, options = {}) {
const { replace = false, scroll = true, restoreScrollY = null } = options;
try {
document.body.classList.add(CONFIG.loadingClass);
let html = pageCache.get(url);
if (!html) {
const response = await fetch(url, {
headers: {
'X-Requested-With': 'What',
'Accept': 'text/html'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
html = await response.text();
pageCache.set(url, html);
setTimeout(() => pageCache.delete(url), CONFIG.cacheTimeout);
}
if (replace) {
history.replaceState({ url }, '', url);
} else {
const current = history.state || {};
history.replaceState(
{
url: current.url || (window.location.pathname + window.location.search),
scrollY: window.scrollY
},
'',
window.location.href
);
history.pushState({ url }, '', url);
}
await swapPageContent(html);
if (restoreScrollY !== null) {
window.scrollTo(0, restoreScrollY);
} else if (scroll) {
window.scrollTo(0, 0);
}
initializeElements();
} catch (error) {
console.error('[What] Navigation error:', error);
window.location.href = url;
} finally {
document.body.classList.remove(CONFIG.loadingClass);
}
}
function handlePopState(event) {
if (event.state && event.state.url) {
const y = typeof event.state.scrollY === 'number' ? event.state.scrollY : 0;
navigateTo(event.state.url, { replace: true, scroll: false, restoreScrollY: y });
}
}
function getStylesheetKey(link) {
return [
link.getAttribute('href') || '',
link.getAttribute('media') || '',
link.getAttribute('rel') || ''
].join('::');
}
function syncHeadInlineStyles(doc) {
const currentTexts = new Set(
Array.from(document.head.querySelectorAll('style')).map(function(s) { return s.textContent; })
);
const nextTexts = new Set(
Array.from(doc.head.querySelectorAll('style')).map(function(s) { return s.textContent; })
);
Array.from(doc.head.querySelectorAll('style')).forEach(function(style) {
if (!currentTexts.has(style.textContent)) {
const clone = document.createElement('style');
clone.textContent = style.textContent;
clone.setAttribute('data-w-synced', '');
document.head.appendChild(clone);
}
});
return Array.from(document.head.querySelectorAll('style[data-w-synced]'))
.filter(function(style) { return !nextTexts.has(style.textContent); });
}
function syncHeadStylesheets(doc) {
const selector = 'link[rel~="stylesheet"][href]';
const nextLinks = Array.from(doc.head.querySelectorAll(selector));
const nextKeys = new Set(nextLinks.map(getStylesheetKey));
const staleLinks = Array.from(document.head.querySelectorAll(selector))
.filter(function(link) { return !nextKeys.has(getStylesheetKey(link)); });
const existingKeys = new Set(
Array.from(document.head.querySelectorAll(selector)).map(getStylesheetKey)
);
const firstScript = document.head.querySelector('script');
var pendingLoads = [];
nextLinks.forEach(function(link) {
const key = getStylesheetKey(link);
if (existingKeys.has(key)) {
return;
}
const clone = link.cloneNode(true);
pendingLoads.push(new Promise(function(resolve) {
clone.onload = resolve;
clone.onerror = resolve;
setTimeout(resolve, 3000);
}));
if (firstScript) {
document.head.insertBefore(clone, firstScript);
} else {
document.head.appendChild(clone);
}
existingKeys.add(key);
});
return {
ready: pendingLoads.length ? Promise.all(pendingLoads) : Promise.resolve(),
staleLinks: staleLinks
};
}
async function swapPageContent(html) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newTitle = doc.querySelector('title');
if (newTitle) {
document.title = newTitle.textContent;
}
var staleStyles = syncHeadInlineStyles(doc);
var cssSync = syncHeadStylesheets(doc);
const newBody = doc.querySelector('body');
if (newBody) {
await cssSync.ready;
Array.from(newBody.attributes).forEach(function(attr) {
document.body.setAttribute(attr.name, attr.value);
});
const persistElements = document.querySelectorAll('[w-persist]');
const persistData = new Map();
persistElements.forEach(el => {
const id = el.getAttribute('w-persist');
persistData.set(id, el.cloneNode(true));
});
document.body.innerHTML = newBody.innerHTML;
cssSync.staleLinks.forEach(function(link) { link.remove(); });
staleStyles.forEach(function(style) { style.remove(); });
persistData.forEach((el, id) => {
const placeholder = document.querySelector(`[w-persist="${id}"]`);
if (placeholder) {
placeholder.replaceWith(el);
}
});
executeScripts(document.body);
}
}
async function handleFormSubmit(event) {
const form = event.target;
if (!form.matches(CONFIG.formSelector)) return;
const confirmMsg = form.getAttribute('w-confirm');
if (confirmMsg && !confirm(confirmMsg)) {
event.preventDefault();
return;
}
const target = form.getAttribute('w-target');
if (target || form.hasAttribute('w-boost')) {
event.preventDefault();
const action = form.getAttribute('action') || window.location.href;
debug('form submit →', action, target ? `(target: ${target})` : '(boost)');
await submitForm(form);
}
}
async function submitForm(form) {
const target = form.getAttribute('w-target');
const swap = form.getAttribute('w-swap') || 'innerHTML';
const loadingClass = form.getAttribute('w-loading') || CONFIG.loadingClass;
const formData = new FormData(form);
const method = (form.getAttribute('method') || 'POST').toUpperCase();
let url = form.getAttribute('action') || window.location.href;
form.classList.add(loadingClass);
const submitBtn = form.querySelector('[type="submit"]');
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.classList.add('btn-loading');
}
try {
const hasFiles = form.querySelector('input[type="file"]') && form.querySelector('input[type="file"]').files.length > 0;
const body = hasFiles ? formData : new URLSearchParams(formData);
const headers = addCsrfHeader({ 'X-Requested-With': 'What' });
if (!hasFiles) headers['Content-Type'] = 'application/x-www-form-urlencoded';
const response = await fetch(url, {
method,
body,
headers,
redirect: 'follow'
});
pageCache.clear();
if (response.redirected) {
navigateTo(response.url, { replace: true });
return;
}
const html = await response.text();
if (target) {
const targetEl = document.querySelector(target);
if (targetEl) {
swapContent(targetEl, html, swap);
}
} else {
await swapPageContent(html);
}
if (response.ok && form.hasAttribute('w-reset')) {
form.reset();
}
var setExpr = form.getAttribute('w-set');
if (setExpr && response.ok) {
fetch('/w-set', {
method: 'POST',
headers: addCsrfHeader({
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'What'
}),
body: 'expr=' + encodeURIComponent(setExpr)
});
}
form.dispatchEvent(new CustomEvent('w:success', {
detail: { response, html }
}));
} catch (error) {
console.error('[What] Form submission error:', error);
form.dispatchEvent(new CustomEvent('w:error', {
detail: { error }
}));
} finally {
form.classList.remove(loadingClass);
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.classList.remove('btn-loading');
}
}
}
function parseWTriggers(value) {
if (!value) return [];
return value.split(',').map(function(part) {
const words = part.trim().split(/\s+/);
if (words[0] === 'poll') {
return { type: 'poll', interval: parseWInterval(words[1]) };
}
return { type: words[0] };
}).filter(function(t) { return t.type; });
}
function parseWInterval(s) {
const match = /^(\d+)(ms|s|m|h)?$/.exec(s || '');
if (!match) return null;
return parseInt(match[1], 10) * { ms: 1, s: 1000, m: 60000, h: 3600000 }[match[2] || 's'];
}
function initFetchTriggers() {
document.querySelectorAll('[w-trigger]').forEach(function(el) {
if (!el.hasAttribute('w-get') && !el.hasAttribute('w-post')) return;
if (wArmedTriggers.has(el)) return;
wArmedTriggers.add(el);
parseWTriggers(el.getAttribute('w-trigger')).forEach(function(trigger) {
if (trigger.type === 'load') {
if ((el.getAttribute('w-swap') || '') === 'outerHTML' && !el.getAttribute('w-target')) {
debug('warn', 'w-trigger="load" with w-swap="outerHTML" on self re-arms every response — possible fetch loop', el);
}
doPartialFetch(el, { confirm: false });
} else if (trigger.type === 'revealed') {
const entry = { el: el, cancel: null };
const observer = new IntersectionObserver(function(entries) {
if (entries.some(function(e) { return e.isIntersecting; })) {
observer.disconnect();
const idx = wFetchLifecycles.indexOf(entry);
if (idx !== -1) wFetchLifecycles.splice(idx, 1);
doPartialFetch(el, { confirm: false });
}
});
entry.cancel = function() { observer.disconnect(); };
observer.observe(el);
wFetchLifecycles.push(entry);
} else if (trigger.type === 'poll') {
if (!trigger.interval) {
debug('warn', 'invalid poll interval in w-trigger:', el.getAttribute('w-trigger'));
return;
}
const entry = { el: el, cancel: null };
const id = setInterval(function() {
if (!el.isConnected) {
entry.cancel();
const idx = wFetchLifecycles.indexOf(entry);
if (idx !== -1) wFetchLifecycles.splice(idx, 1);
return;
}
if (document.hidden || el._wFetchBusy) return;
doPartialFetch(el, { confirm: false, silentError: true });
}, trigger.interval);
entry.cancel = function() { clearInterval(id); };
wFetchLifecycles.push(entry);
}
});
});
}
function sweepFetchLifecycles() {
for (let i = wFetchLifecycles.length - 1; i >= 0; i--) {
if (!wFetchLifecycles[i].el.isConnected) {
wFetchLifecycles[i].cancel();
wFetchLifecycles.splice(i, 1);
}
}
}
function handleTriggerClick(event) {
const trigger = event.target.closest('[w-trigger="click"]');
if (!trigger) return;
event.preventDefault();
executeTrigger(trigger);
}
function handleTriggerChange(event) {
const trigger = event.target.closest('[w-trigger="change"]');
if (!trigger) return;
executeTrigger(trigger);
}
async function handlePartialFetch(event) {
const element = event.target.closest('[w-get], [w-post]');
if (!element) return;
if (element.hasAttribute('w-set')) return;
const triggers = parseWTriggers(element.getAttribute('w-trigger'));
if (triggers.length && !triggers.some(function(t) { return t.type === 'click'; })) return;
event.preventDefault();
await doPartialFetch(element);
}
async function doPartialFetch(element, opts = {}) {
const url = element.getAttribute('w-get') || element.getAttribute('w-post');
const method = element.hasAttribute('w-post') ? 'POST' : 'GET';
const target = element.getAttribute('w-target');
const swap = element.getAttribute('w-swap') || 'innerHTML';
const confirmMsg = element.getAttribute('w-confirm');
const loadingClass = element.getAttribute('w-loading') || CONFIG.loadingClass;
const paramsAttr = element.getAttribute('w-params');
const includeSelector = element.getAttribute('w-include');
const targetEl = (target && target !== 'this') ? document.querySelector(target) : element;
if (!targetEl) {
console.warn('[What] Target not found:', target);
return;
}
if (confirmMsg && opts.confirm !== false && !window.confirm(confirmMsg)) {
return;
}
element.classList.add(loadingClass);
targetEl.classList.add(loadingClass);
element._wFetchBusy = true;
try {
const options = {
method,
headers: addCsrfHeader({
'X-Requested-With': 'What',
'Accept': 'text/html'
})
};
let params = new URLSearchParams();
if (paramsAttr) {
try {
const paramsObj = JSON.parse(paramsAttr);
Object.entries(paramsObj).forEach(([k, v]) => params.append(k, v));
} catch (e) {
console.warn('[What] Invalid w-params JSON:', paramsAttr);
}
}
if (includeSelector) {
const form = document.querySelector(includeSelector);
if (form) {
const formData = new FormData(form);
formData.forEach((v, k) => params.append(k, v));
}
}
let finalUrl = url;
if (method === 'GET' && params.toString()) {
finalUrl = url + (url.includes('?') ? '&' : '?') + params.toString();
} else if (method === 'POST') {
options.body = params;
options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
const response = await fetch(finalUrl, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const html = await response.text();
const { cleanHtml, updates } = extractOobUpdates(html);
debug(`inject ${swap} →`, target, `(from ${method} ${url})`);
const preservesModes = ['prepend', 'afterbegin', 'append', 'beforeend', 'before', 'beforebegin', 'after', 'afterend'];
if (preservesModes.includes(swap)) {
const temp = document.createElement('div');
temp.innerHTML = cleanHtml;
temp.querySelectorAll('[w-bind]').forEach(el => el.removeAttribute('w-bind'));
swapContent(targetEl, temp.innerHTML, swap);
} else {
swapContent(targetEl, cleanHtml, swap);
targetEl.querySelectorAll('[w-bind]').forEach(el => {
var bind = el.getAttribute('w-bind');
if (bind && !bind.startsWith('wired.')) {
el.removeAttribute('w-bind');
}
});
}
if (updates) {
debug('applying OOB updates:', updates);
applyDataBindUpdates(updates);
}
initializeElements();
element.classList.remove('w-fetch-error');
targetEl.dispatchEvent(new CustomEvent('w:load', {
bubbles: true,
detail: { url, method, swap, updates }
}));
} catch (error) {
console.error('[What] Partial fetch error:', error);
element.classList.add('w-fetch-error');
element.dispatchEvent(new CustomEvent('w:error', {
bubbles: true,
detail: { url, method, error: String(error) }
}));
if (!opts.silentError) {
targetEl.innerHTML = `<div class="text-red-600 p-4">Failed to load content</div>`;
}
} finally {
element._wFetchBusy = false;
element.classList.remove(loadingClass);
targetEl.classList.remove(loadingClass);
}
}
async function handleWSet(event) {
const el = event.target.closest('[w-set]');
if (!el) return;
if (el.tagName === 'FORM') return;
event.preventDefault();
const rawExpr = el.getAttribute('w-set');
const target = el.getAttribute('w-target');
const swap = el.getAttribute('w-swap') || 'innerHTML';
const loadingClass = el.getAttribute('w-loading') || CONFIG.loadingClass;
var expr = rawExpr;
if (el.value !== undefined && rawExpr.indexOf('$value') !== -1) {
var escaped = el.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
expr = rawExpr.replace(/\$value/g, '"' + escaped + '"');
}
el.classList.add(loadingClass);
try {
const formData = new URLSearchParams();
formData.append('expr', expr);
const fetchHeaders = addCsrfHeader({
'X-Requested-With': 'What',
'Content-Type': 'application/x-www-form-urlencoded'
});
const response = await fetch('/w-set', {
method: 'POST',
body: formData,
headers: fetchHeaders
});
pageCache.clear();
if (response.redirected) {
navigateTo(response.url, { replace: true });
return;
}
const html = await response.text();
if (!html) return;
const result = extractOobUpdates(html);
if (result.updates) {
debug('w-set updates:', result.updates);
applyDataBindUpdates(result.updates);
}
if (target) {
const targetEl = document.querySelector(target);
if (targetEl) {
swapContent(targetEl, result.cleanHtml, swap);
}
}
const partialUrl = el.getAttribute('w-get') || el.getAttribute('w-post');
if (partialUrl && target) {
const partialMethod = el.hasAttribute('w-post') ? 'POST' : 'GET';
const partialHeaders = addCsrfHeader({
'X-Requested-With': 'What',
'Accept': 'text/html'
});
const partialResponse = await fetch(partialUrl, {
method: partialMethod,
headers: partialHeaders
});
if (partialResponse.ok) {
const partialHtml = await partialResponse.text();
const partialResult = extractOobUpdates(partialHtml);
const targetEl = document.querySelector(target);
if (targetEl) {
swapContent(targetEl, partialResult.cleanHtml, swap);
}
if (partialResult.updates) {
applyDataBindUpdates(partialResult.updates);
}
}
}
} catch (error) {
console.error('[What] w-set error:', error);
} finally {
el.classList.remove(loadingClass);
}
}
var _wSetInputTimers = new WeakMap();
function handleWSetInput(event) {
var el = event.target.closest('[w-set]');
if (!el) return;
if (el.value === undefined) return;
clearTimeout(_wSetInputTimers.get(el));
_wSetInputTimers.set(el, setTimeout(function() {
handleWSet(event);
}, 300));
}
function initWire() {
var hasBindings = document.querySelector('[w-bind^="wired."], [data-w-src^="wired."], [data-w-href^="wired."], [w-watch^="wired."]');
var statusEls = document.querySelectorAll('.w-wire-status');
if (!hasBindings && statusEls.length === 0) return;
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
var url = proto + '//' + location.host + '/w-wire';
var ws = new WebSocket(url);
var reconnectDelay = 1000;
function setStatusConnected(connected) {
statusEls.forEach(function(el) {
if (connected) el.classList.add('connected');
else el.classList.remove('connected');
});
}
ws.onopen = function() {
debug('Wired WebSocket connected');
reconnectDelay = 1000;
setStatusConnected(true);
};
ws.onmessage = function(event) {
try {
var data = JSON.parse(event.data);
if (data.type === 'connected') {
setStatusConnected(true);
return;
}
debug('Wired update:', data);
applyDataBindUpdates(data);
Object.keys(data).forEach(function(path) {
document.querySelectorAll('[w-watch~="' + path + '"]').forEach(function(el) {
if (el.hasAttribute('w-get') || el.hasAttribute('w-post')) {
doPartialFetch(el, { confirm: false });
}
});
});
} catch (e) {
}
};
ws.onclose = function() {
debug('Wired WebSocket closed, reconnecting in ' + reconnectDelay + 'ms');
setStatusConnected(false);
setTimeout(function() {
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
initWire();
}, reconnectDelay);
};
ws.onerror = function() {
ws.close();
};
}
async function executeTrigger(element) {
const action = element.getAttribute('w-action');
const url = element.getAttribute('w-url') || element.getAttribute('href');
const target = element.getAttribute('w-target');
const swap = element.getAttribute('w-swap') || 'innerHTML';
const confirm = element.getAttribute('w-confirm');
const loadingClass = element.getAttribute('w-loading') || CONFIG.loadingClass;
if (confirm && !window.confirm(confirm)) {
return;
}
element.classList.add(loadingClass);
try {
switch (action) {
case 'toggle':
toggleElement(element.getAttribute('w-toggle'));
break;
case 'remove':
removeElement(target || element);
break;
case 'navigate':
if (url) navigateTo(url);
break;
case 'delete':
await executeDelete(url, target, swap);
break;
default:
if (url) {
const html = await fetchContent(url);
if (target) {
const targetEl = document.querySelector(target);
if (targetEl) swapContent(targetEl, html, swap);
}
}
}
} catch (error) {
console.error('[What] Trigger error:', error);
} finally {
element.classList.remove(loadingClass);
}
}
async function executeDelete(url, target, swap) {
const response = await fetch(url, {
method: 'POST',
headers: addCsrfHeader({
'X-Requested-With': 'What'
}),
body: new URLSearchParams({ 'w-action': 'delete' })
});
if (response.redirected) {
navigateTo(response.url, { replace: true });
} else if (target) {
const targetEl = document.querySelector(target);
if (targetEl) {
targetEl.remove();
}
}
}
function applyDataBindUpdates(updates) {
if (!updates || typeof updates !== 'object') {
return;
}
if (DEBUG) console.group('[What] Live Updates');
for (const [path, value] of Object.entries(updates)) {
const elements = document.querySelectorAll(`[w-bind="${path}"]`);
if (DEBUG) debug('verbose', `w-bind="${path}" → "${value}" (${elements.length} elements)`);
elements.forEach(el => {
var tag = el.tagName;
if (tag === 'IMG' || tag === 'SOURCE' || tag === 'VIDEO' || tag === 'AUDIO' || tag === 'IFRAME') {
el.setAttribute('src', String(value));
} else if (tag === 'A') {
el.setAttribute('href', String(value));
} else {
el.textContent = String(value);
}
});
var srcEls = document.querySelectorAll(`[data-w-src="${path}"]`);
srcEls.forEach(el => {
el.setAttribute('src', String(value));
});
var hrefEls = document.querySelectorAll(`[data-w-href="${path}"]`);
hrefEls.forEach(el => {
el.setAttribute('href', String(value));
});
}
if (DEBUG) console.groupEnd();
}
function extractOobUpdates(html) {
const templateRegex = /<template\s+data-what-updates>([\s\S]*?)<\/template>/i;
const match = html.match(templateRegex);
if (!match) {
return { cleanHtml: html, updates: null };
}
const cleanHtml = html.replace(match[0], '');
try {
const updates = JSON.parse(match[1]);
return { cleanHtml, updates };
} catch (e) {
console.warn('[What] Invalid OOB updates JSON:', match[1]);
return { cleanHtml, updates: null };
}
}
async function fetchContent(url) {
const response = await fetch(url, {
headers: {
'X-Requested-With': 'What',
'Accept': 'text/html'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.text();
}
function swapContent(target, html, mode = 'replace') {
switch (mode) {
case 'replace':
case 'innerHTML':
case 'inner':
target.innerHTML = html;
break;
case 'outerHTML':
case 'outer':
target.outerHTML = html;
break;
case 'beforebegin':
case 'before':
target.insertAdjacentHTML('beforebegin', html);
break;
case 'afterbegin':
case 'prepend':
target.insertAdjacentHTML('afterbegin', html);
break;
case 'beforeend':
case 'append':
target.insertAdjacentHTML('beforeend', html);
break;
case 'afterend':
case 'after':
target.insertAdjacentHTML('afterend', html);
break;
case 'none':
break;
}
executeScripts(target);
initializeElements();
var scrollEl = target.hasAttribute('w-scroll') ? target : target.closest('[w-scroll]');
if (scrollEl) {
var dir = scrollEl.getAttribute('w-scroll');
if (dir === 'bottom') scrollEl.scrollTop = scrollEl.scrollHeight;
else if (dir === 'top') scrollEl.scrollTop = 0;
}
}
function executeScripts(container) {
const scripts = container.querySelectorAll('script');
scripts.forEach(oldScript => {
var src = oldScript.getAttribute('src');
if (src) {
try {
var url = new URL(src, window.location.origin);
if (url.origin !== window.location.origin) {
debug('warn', 'Blocked external script:', src);
oldScript.remove();
return;
}
} catch (e) {
oldScript.remove();
return;
}
}
const newScript = document.createElement('script');
Array.from(oldScript.attributes).forEach(attr => {
newScript.setAttribute(attr.name, attr.value);
});
newScript.textContent = oldScript.textContent;
oldScript.parentNode.replaceChild(newScript, oldScript);
});
}
function toggleElement(selector) {
const element = document.querySelector(selector);
if (element) {
element.classList.toggle('hidden');
}
}
function removeElement(selectorOrElement) {
const element = typeof selectorOrElement === 'string'
? document.querySelector(selectorOrElement)
: selectorOrElement;
if (element) {
element.remove();
}
}
function toggleModal(modal, show) {
if (show) {
modal.classList.add(CONFIG.activeClass);
document.body.style.overflow = 'hidden';
} else {
modal.classList.remove(CONFIG.activeClass);
document.body.style.overflow = '';
}
}
let liveReloadSocket = null;
let liveReloadReconnectTimer = null;
let liveReloadEnabled = false;
function connectLiveReload() {
if (liveReloadSocket && liveReloadSocket.readyState === WebSocket.OPEN) {
return; }
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/w-livereload`;
try {
liveReloadSocket = new WebSocket(wsUrl);
liveReloadSocket.onopen = function() {
DEBUG = true;
console.log('[What] DEVELOPMENT mode - debug logging enabled');
console.log('[What] Live reload connected');
liveReloadEnabled = true;
if (liveReloadReconnectTimer) {
clearTimeout(liveReloadReconnectTimer);
liveReloadReconnectTimer = null;
}
};
liveReloadSocket.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
if (data.type === 'reload') {
console.log('[What] Reloading page...');
pageCache.clear();
window.location.reload();
} else if (data.type === 'cache_cleared') {
console.log('[What] Server cache cleared');
pageCache.clear();
} else if (data.type === 'connected') {
console.log('[What] Live reload ready');
}
} catch (e) {
console.warn('[What] Invalid live reload message:', event.data);
}
};
liveReloadSocket.onclose = function() {
liveReloadEnabled = false;
if (!liveReloadReconnectTimer) {
liveReloadReconnectTimer = setTimeout(() => {
liveReloadReconnectTimer = null;
console.log('[What] Attempting to reconnect live reload...');
connectLiveReload();
}, 2000);
}
};
liveReloadSocket.onerror = function() {
liveReloadSocket.close();
};
} catch (e) {
console.warn('[What] Live reload not available:', e.message);
}
}
function disconnectLiveReload() {
if (liveReloadReconnectTimer) {
clearTimeout(liveReloadReconnectTimer);
liveReloadReconnectTimer = null;
}
if (liveReloadSocket) {
liveReloadSocket.close();
liveReloadSocket = null;
}
liveReloadEnabled = false;
}
window.What = {
init,
navigateTo,
swapContent,
toggleModal,
submitForm,
applyDataBindUpdates,
extractOobUpdates,
clearCache: () => {
pageCache.clear();
console.log('[What] Local cache cleared');
},
showCache: () => {
const entries = Array.from(pageCache.keys());
if (entries.length === 0) {
console.log('[What] Cache is empty');
return [];
}
console.log('[What] Cached pages:');
entries.forEach((url, i) => {
console.log(` ${i + 1}. ${url}`);
});
return entries;
},
clearAllCaches: async () => {
pageCache.clear();
console.log('[What] Local cache cleared');
try {
const response = await fetch('/w-cache/clear-all', {
method: 'POST',
headers: addCsrfHeader({ 'Content-Type': 'application/json' })
});
if (response.ok) {
const result = await response.json();
console.log('[What] Server cache cleared:', result.message);
return { local: true, server: true };
} else if (response.status === 404) {
console.log('[What] Server cache clear not available (production mode)');
return { local: true, server: false };
} else {
console.warn('[What] Failed to clear server cache');
return { local: true, server: false };
}
} catch (e) {
console.warn('[What] Error clearing server cache:', e.message);
return { local: true, server: false };
}
},
sessions: async () => {
try {
const response = await fetch('/w-sessions/list');
if (response.ok) {
const result = await response.json();
console.log(`[What] Active sessions: ${result.count}`);
if (result.ids && result.ids.length > 0) {
result.ids.forEach((id, i) => {
console.log(` ${i + 1}. ${id.substring(0, 16)}...`);
});
}
return result;
} else if (response.status === 404) {
console.log('[What] Session list not available (production mode)');
return { count: 0, ids: [] };
} else {
console.warn('[What] Failed to get session list');
return { count: 0, ids: [] };
}
} catch (e) {
console.warn('[What] Error getting session list:', e.message);
return { count: 0, ids: [] };
}
},
clearSessionData: async () => {
try {
const response = await fetch('/w-session/clear-data', {
method: 'POST',
headers: addCsrfHeader({ 'Content-Type': 'application/json' })
});
if (response.ok) {
const result = await response.json();
console.log('[What] Session data cleared');
return result;
} else {
console.warn('[What] Failed to clear session data');
return { success: false };
}
} catch (e) {
console.warn('[What] Error clearing session data:', e.message);
return { success: false };
}
},
data: {
show: async () => {
try {
const response = await fetch('/w-data/info');
if (response.ok) {
const result = await response.json();
console.log('[What] Data Store:');
console.log(' Application:', result.application);
console.log(' Session:', result.session);
return result;
} else if (response.status === 404) {
console.log('[What] Data info not available (production mode)');
return { application: {}, session: {} };
} else {
console.warn('[What] Failed to get data info');
return { application: {}, session: {} };
}
} catch (e) {
console.warn('[What] Error getting data info:', e.message);
return { application: {}, session: {} };
}
}
},
inspect: () => { window.open('/w-inspector', '_blank'); },
connectLiveReload,
disconnectLiveReload,
isLiveReloadEnabled: () => liveReloadEnabled
};
function initFormValidation() {
document.querySelectorAll('form[w-validate]').forEach(form => {
if (form._whatValidationBound) return;
form._whatValidationBound = true;
form.addEventListener('submit', handleFormValidation);
form.querySelectorAll('input, textarea, select').forEach(input => {
if (input.type === 'hidden' || input.type === 'submit') return;
input.addEventListener('blur', () => validateSingleField(form, input));
});
});
}
function handleFormValidation(e) {
const form = e.target;
const rules = decodeRulesFromForm(form);
if (!rules || !rules.fields) return;
const errors = {};
for (const [fieldName, fieldRules] of Object.entries(rules.fields)) {
const input = form.querySelector('[name="' + fieldName + '"]');
if (!input) continue;
const value = input.value.trim();
const error = validateFieldValue(value, fieldRules, form);
if (error) errors[fieldName] = error;
}
if (Object.keys(errors).length > 0) {
e.preventDefault();
showValidationErrors(form, errors);
} else {
clearValidationErrors(form);
}
}
function validateSingleField(form, input) {
const rules = decodeRulesFromForm(form);
if (!rules || !rules.fields) return;
const fieldName = input.getAttribute('name');
const fieldRules = rules.fields[fieldName];
if (!fieldRules) return;
const value = input.value.trim();
const error = validateFieldValue(value, fieldRules, form);
clearFieldError(input);
if (error) {
showFieldError(input, error);
}
}
function decodeRulesFromForm(form) {
const hidden = form.querySelector('input[name="w-rules"]');
if (!hidden) return null;
try {
const parts = hidden.value.split('.');
if (parts.length !== 3) return null;
const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
return payload;
} catch(e) {
return null;
}
}
function validateFieldValue(value, rules, form) {
if (rules.required && !value) {
return rules.error_message || 'This field is required';
}
if (!value) return null;
if (rules.min && value.length < rules.min) {
return rules.error_message || 'Must be at least ' + rules.min + ' characters';
}
if (rules.max && value.length > rules.max) {
return rules.error_message || 'Must be at most ' + rules.max + ' characters';
}
if (rules.field_type) {
switch (rules.field_type) {
case 'email':
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
return rules.error_message || 'Invalid email address';
break;
case 'url':
try { new URL(value); } catch {
return rules.error_message || 'Invalid URL';
}
break;
case 'number':
if (isNaN(Number(value)))
return rules.error_message || 'Must be a number';
break;
case 'phone':
if (!/^\+?[\d\s\-()]{7,20}$/.test(value))
return rules.error_message || 'Invalid phone number';
break;
case 'date':
if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
return rules.error_message || 'Invalid date (YYYY-MM-DD)';
break;
case 'time':
if (!/^\d{2}:\d{2}(:\d{2})?$/.test(value))
return rules.error_message || 'Invalid time (HH:MM)';
break;
}
}
if (rules.pattern) {
if (rules.pattern.length > 500) {
return rules.error_message || 'Invalid format';
}
try {
if (!new RegExp(rules.pattern).test(value)) {
return rules.error_message || 'Invalid format';
}
} catch(e) { }
}
if (rules.match_field) {
const other = form.querySelector('[name="' + rules.match_field + '"]');
if (other && value !== other.value.trim()) {
return rules.error_message || 'Must match ' + rules.match_field;
}
}
return null;
}
function showValidationErrors(form, errors) {
clearValidationErrors(form);
for (const [field, message] of Object.entries(errors)) {
const input = form.querySelector('[name="' + field + '"]');
if (input) showFieldError(input, message);
}
const firstError = form.querySelector('.w-invalid');
if (firstError) firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
function showFieldError(input, message) {
input.classList.add('w-invalid');
const errorEl = document.createElement('div');
errorEl.className = 'w-field-error';
errorEl.textContent = message;
input.parentNode.insertBefore(errorEl, input.nextSibling);
}
function clearFieldError(input) {
input.classList.remove('w-invalid');
const next = input.nextElementSibling;
if (next && next.classList.contains('w-field-error')) {
next.remove();
}
}
function clearValidationErrors(form) {
form.querySelectorAll('.w-field-error').forEach(el => el.remove());
form.querySelectorAll('.w-invalid').forEach(el => el.classList.remove('w-invalid'));
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
init();
if (_debugMeta) connectLiveReload();
});
} else {
init();
if (_debugMeta) connectLiveReload();
}
})();