escapeHtml now escapes & < > " ' (shared web/js/html-escape.js, loaded by admin, gallery and metadata pages) instead of the textContent/innerHTML round-trip that left quotes intact, so a tag, category, checkpoint or file name containing a quote can no longer break out of a data-*, value or title attribute. Inline onclick handlers that interpolated server data now carry data-action and data-* attributes and are dispatched through one delegated listener per page (web/js/data-actions.js); the img onerror fallback uses data-fallback-src with a capturing error listener. Every remaining template value written into an attribute or text node (prompts, checkpoints, file paths, log lines, directory names, categories, suggestions) is escaped. metadata.html no longer builds an "explorer /select" shell command from the file name (Electron-only branch); the path is copied to the clipboard.
52 lines
1.9 KiB
JavaScript
52 lines
1.9 KiB
JavaScript
/**
|
|
* Dispatch for elements rendered with `data-action="..."`.
|
|
*
|
|
* The admin and gallery pages build HTML from server data with template
|
|
* literals. Inline `onclick="...${value}..."` handlers would put that data
|
|
* inside JavaScript source, where HTML escaping does not help; instead the
|
|
* markup carries `data-*` attributes and one delegated click listener per page
|
|
* calls `dispatch(element.dataset, handlers, element)`.
|
|
*
|
|
* Loadable in the browser (window.DataActions) and in Node for tests.
|
|
*/
|
|
(function (root) {
|
|
"use strict";
|
|
|
|
function toInt(value) {
|
|
if (value === undefined || value === null || value === "") return null;
|
|
const n = parseInt(value, 10);
|
|
return Number.isNaN(n) ? null : n;
|
|
}
|
|
|
|
/**
|
|
* @param {DOMStringMap|Object|null|undefined} dataset
|
|
* @returns {{action: string, promptId: number|null, index: number|null} & Object<string, string>}
|
|
*/
|
|
function readAction(dataset) {
|
|
const data = dataset || {};
|
|
return {
|
|
...data,
|
|
action: typeof data.action === "string" ? data.action : "",
|
|
promptId: toInt(data.promptId),
|
|
index: toInt(data.index),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {DOMStringMap|Object} dataset - the clicked element's dataset
|
|
* @param {Object<string, Function>} handlers - handler per action name
|
|
* @param {...*} extra - passed to the handler after the parsed action
|
|
* @returns {boolean} whether a handler ran
|
|
*/
|
|
function dispatch(dataset, handlers, ...extra) {
|
|
const parsed = readAction(dataset);
|
|
if (!parsed.action || !Object.prototype.hasOwnProperty.call(handlers, parsed.action)) return false;
|
|
handlers[parsed.action](parsed, ...extra);
|
|
return true;
|
|
}
|
|
|
|
const api = { readAction, dispatch };
|
|
if (typeof module !== "undefined" && module.exports) module.exports = api;
|
|
else root.DataActions = api;
|
|
})(typeof window !== "undefined" ? window : globalThis);
|