-
🌐 async-utils.jsWeb, 3.69 kB⬇
-
🌐 auth-storage.jsWeb, 735 B⬇
-
🌐 chat-helpers.jsWeb, 8.85 kB⬇
-
🌐 chat-renderer.jsWeb, 70.00 kB⬇
-
🌐 code-overlay.jsWeb, 21.77 kB⬇
-
🌐 confirm-dialog.jsWeb, 3.17 kB⬇
-
🌐 conversation-format-utils.jsWeb, 21.51 kB⬇
-
🌐 diagram-overlay.jsWeb, 7.93 kB⬇
-
🌐 dialog.jsWeb, 2.50 kB⬇
-
🌐 dom-utils.jsWeb, 8.33 kB⬇
-
🌐 drag-reorder.jsWeb, 4.82 kB⬇
-
🌐 elements.jsWeb, 11.06 kB⬇
-
🌐 export-utils.jsWeb, 1.53 kB⬇
-
🌐 format-utils.jsWeb, 29.60 kB⬇
-
🌐 history-navigation.jsWeb, 1.95 kB⬇
-
🌐 hljs-yaml.jsWeb, 5.07 kB⬇
-
🌐 inline-format.jsWeb, 7.35 kB⬇
-
🌐 instant-answer.jsWeb, 19.82 kB⬇
-
🌐 interactive-tools.jsWeb, 38.77 kB⬇
-
🌐 mermaid-fixes.jsWeb, 4.60 kB⬇
-
🌐 mermaid-render.jsWeb, 5.10 kB⬇
-
🌐 mermaid.jsWeb, 195 B⬇
-
🌐 migrations.jsWeb, 12.11 kB⬇
-
🌐 multi-choice-dialog.jsWeb, 1.33 kB⬇
-
🌐 notification-stack.jsWeb, 2.50 kB⬇
-
🌐 options-manager.jsWeb, 45.56 kB⬇
-
🌐 overlay-utils.jsWeb, 4.14 kB⬇
-
🌐 pdf-export.jsWeb, 23.59 kB⬇
-
🌐 pinch-zoom.jsWeb, 3.21 kB⬇
-
🌐 plugin-dialog.jsWeb, 2.17 kB⬇
-
🌐 read-aloud.jsWeb, 33.84 kB⬇
-
🌐 response-pipeline.jsWeb, 55.19 kB⬇
-
🌐 scratchpad-editor.jsWeb, 8.64 kB⬇
-
🌐 scratchpad-export.jsWeb, 3.56 kB⬇
-
🌐 scrollbars.jsWeb, 3.12 kB⬇
-
🌐 selection-manager.jsWeb, 3.76 kB⬇
-
🌐 settings-utils.jsWeb, 31.08 kB⬇
-
🌐 share.jsWeb, 10.75 kB⬇
-
🌐 side-drawer.jsWeb, 9.37 kB⬇
-
🌐 speech-text.jsWeb, 6.60 kB⬇
-
🌐 swipe-navigation.jsWeb, 6.16 kB⬇
-
🌐 table-overlay.jsWeb, 8.10 kB⬇
-
🌐 theme-utils.jsWeb, 8.34 kB⬇
-
🌐 thinking-panel-base.jsWeb, 14.88 kB⬇
-
🌐 ui-sets.jsWeb, 1.00 kB⬇
-
🌐 version-overlay.jsWeb, 4.63 kB⬇
/**
* DOM utility helpers for element selection, manipulation, and safe event handling.
*/
/**
* Safely gets an element by its ID.
* @param {string} id - Element ID.
* @returns {HTMLElement|null} The DOM element or null if not found.
*/
export function getEl(id) {
if (!id || typeof id !== 'string') return null;
return document.getElementById(id);
}
/**
* Safely queries a single descendant element.
* @param {string} selector - CSS selector.
* @param {Element|Document} [parent=document] - Parent element to query from.
* @returns {HTMLElement|Element|null} The matched element or null.
*/
export function queryEl(selector, parent = document) {
if (!selector || !parent || typeof parent.querySelector !== 'function') return null;
return parent.querySelector(selector);
}
/**
* Safely queries all matching descendant elements as an Array.
* @param {string} selector - CSS selector.
* @param {Element|Document} [parent=document] - Parent element to query from.
* @returns {Element[]} Array of matched elements.
*/
export function queryAll(selector, parent = document) {
if (!selector || !parent || typeof parent.querySelectorAll !== 'function') return [];
return [...parent.querySelectorAll(selector)];
}
/**
* Toggles a class on an element if the element exists.
* @param {Element|null} el - Target element.
* @param {string} className - Class name to toggle.
* @param {boolean} [force] - Optional force boolean.
* @returns {boolean} Whether the class is present after toggling.
*/
export function toggleElementClass(el, className, force) {
if (!el || !el.classList || !className) return false;
if (typeof force === 'boolean') {
return el.classList.toggle(className, force);
}
return el.classList.toggle(className);
}
/**
* Sets the display style on an element.
* @param {HTMLElement|Element|null} el - Target element.
* @param {string} display - CSS display value.
*/
export function setElementDisplay(el, display) {
if (el && el.style) {
el.style.display = display;
}
}
/**
* Creates a DOM element from an HTML string.
* @param {string} htmlString - Valid HTML string.
* @returns {HTMLElement|Element|null} The parsed root element or null.
*/
export function createElementFromHTML(htmlString) {
if (!htmlString || typeof htmlString !== 'string') return null;
const template = document.createElement('template');
template.innerHTML = htmlString.trim();
return template.content.firstElementChild;
}
/**
* Safely attaches an event listener to an element with existence verification.
* @param {EventTarget|null} el - Target element or event target.
* @param {string} event - Event name.
* @param {Function} handler - Event handler function.
* @param {object|boolean} [options] - Optional addEventListener options.
*/
export function addSafeEventListener(el, event, handler, options) {
if (el && typeof el.addEventListener === 'function' && typeof handler === 'function') {
el.addEventListener(event, handler, options);
}
}
/**
* Updates a range input's --range-fill custom property to the percentage of
* its track that is filled. The themed slider track gradient (WebKit) reads
* this variable; Firefox paints the fill via -moz-range-progress instead.
* @param {HTMLInputElement|null} input - The range input.
*/
export function update_range_fill(input) {
if (!input || input.type !== 'range') return;
const min = parseFloat(input.min) || 0;
const max = parseFloat(input.max);
const value = parseFloat(input.value);
let percent = 0;
if (Number.isFinite(max) && max > min && Number.isFinite(value)) {
percent = ((value - min) / (max - min)) * 100;
}
input.style.setProperty('--range-fill', Math.min(100, Math.max(0, percent)) + '%');
}
/**
* Renders the trailing tokens of a live token stream as spans with an
* increasing opacity fade (oldest 20% to newest 100%).
* @param {HTMLElement|null} container - Target element; left empty when there are no tokens.
* @param {string[]} tokens - All tokens received so far.
* @param {number} [count=5] - Number of trailing tokens to display.
*/
export function render_token_fade(container, tokens, count = 5) {
if (!container) return;
container.innerHTML = '';
const last = Array.isArray(tokens) ? tokens.slice(-count) : [];
const opacities = [0.2, 0.4, 0.6, 0.8, 1];
const offset = opacities.length - last.length;
last.forEach((token, i) => {
const span = document.createElement('span');
span.textContent = token;
span.style.opacity = String(opacities[offset + i]);
container.appendChild(span);
});
}
/**
* Strips HTML tags from a string, leaving the text content.
* @param {string} html - The html string.
* @returns {string} The plain text.
*/
export function strip_tags(html) {
const holder = document.createElement('div');
holder.innerHTML = html || '';
return holder.textContent || '';
}
/**
* Downloads content as a file through a temporary blob URL.
* @param {string} name - The download filename (with extension).
* @param {string} mime - The content MIME type.
* @param {string|ArrayBufferView} content - The file content.
*/
export function download_file(name, mime, content) {
const blob = new Blob([content], { type: mime });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = name;
link.click();
URL.revokeObjectURL(url);
}
let measure_ctx = null;
/**
* Reports whether a label fits inside an element at its current width,
* measured with the element's own font, padding and border.
* @param {HTMLElement} el - The element whose width and font are measured.
* @param {string} text - The label to measure.
* @param {number} extra_em - Extra allowance in ems for chrome such as a
* select's picker icon.
* @returns {boolean} True when the label fits.
*/
function label_fits(el, text, extra_em) {
const cs = getComputedStyle(el);
measure_ctx ||= document.createElement('canvas').getContext('2d');
measure_ctx.font = `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;
const box = parseFloat(cs.paddingLeft) + parseFloat(cs.paddingRight)
+ parseFloat(cs.borderLeftWidth) + parseFloat(cs.borderRightWidth)
+ parseFloat(cs.fontSize) * extra_em;
return el.clientWidth >= measure_ctx.measureText(text.toUpperCase()).width + box;
}
/**
* Swaps a base-select's placeholder option text to a compact label while the
* control is too narrow to fit the full label next to its picker icon, and
* back when space returns. The fit is measured with the select's own font.
* @param {HTMLSelectElement|null} select - The select to watch.
* @param {string} full - The full placeholder label.
* @param {string} compact - The compact label for tight widths.
*/
export function watch_compact_select_label(select, full, compact) {
if (!select) return;
const placeholder = select.querySelector('option[value=""]');
if (!placeholder) return;
// picker icon plus its gap, roughly one and a half em
new ResizeObserver(() => {
placeholder.textContent = label_fits(select, full, 1.6) ? full : compact;
}).observe(select);
}
/**
* Swaps a button's label to a compact label while the button is too narrow to
* fit the full label, and back when space returns. A label set to anything
* else at runtime (e.g. Edit for the preview toggle) is left untouched.
* @param {HTMLButtonElement|null} button - The button to watch.
* @param {string} full - The full label.
* @param {string} compact - The compact label for tight widths.
*/
export function watch_compact_button_label(button, full, compact) {
if (!button) return;
const swap = () => {
const want = label_fits(button, full, 0) ? full : compact;
if ((button.textContent === full || button.textContent === compact) && button.textContent !== want) {
button.textContent = want;
}
};
// childList mutations re-run the swap after a runtime label change such as
// the Edit/Preview toggle, without waiting for a resize
new MutationObserver(swap).observe(button, { childList: true });
new ResizeObserver(swap).observe(button);
}
/**
* Returns whether the keyboard focus is inside any of the given roots.
* Shared by the editors' contains_focus checks guarding panel state and
* saves against unfocused rebuilds.
* @param {...(HTMLElement|null)} roots - The container elements to test.
* @returns {boolean} True when document.activeElement lies inside one of them.
*/
export function contains_focus(...roots) {
return roots.some(root => !!(root && root.contains(document.activeElement)));
}
dom-utils.js
×
Type: Web, text/plain
8.33 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File
Type: Web, text/plain
8.33 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File