-
🌐 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⬇
/**
* Overlay layer for viewing conversation tables enlarged with an as-you-type
* case-insensitive row filter, term highlighting, and click-to-sort headers.
*/
import { showOverlay, hideOverlay } from './overlay-utils.js';
import { addSafeEventListener, getEl, queryAll, queryEl } from './dom-utils.js';
import { content_of } from './scrollbars.js';
let div_overlay = null;
let div_title = null;
let div_view = null;
let input_filter = null;
let table_data = null;
/**
* Currently known filter and sort parameters. Held in memory only, so they
* persist across overlay opens until the app is closed.
*/
const table_params = {
filter: '',
sort_col: -1,
sort_dir: 'asc'
};
/**
* Checks whether the table overlay is currently open.
* @returns {boolean} True if the overlay is active.
*/
export function is_table_overlay_open() {
return !!div_overlay && div_overlay.classList.contains('active');
}
/**
* Opens the overlay showing the given table JSON structure.
* @param {{caption?: string, 'has-header'?: boolean, 'table-rows'?: Array<Array>}} data - The table structure.
*/
export function open_table_overlay(data) {
if (!data || !Array.isArray(data['table-rows'])) return;
div_overlay = getEl('id-table-overlay');
div_title = getEl('id-table-overlay-title');
div_view = content_of(getEl('id-table-overlay-view'));
input_filter = getEl('id-input-table-overlay-filter');
if (!div_overlay || !div_view || !input_filter) return;
table_data = data;
input_filter.value = table_params.filter;
render_table();
showOverlay(div_overlay, 'flex');
}
/**
* Closes the table overlay if it is open.
*/
export function close_table_overlay() {
if (!is_table_overlay_open()) return;
hideOverlay(div_overlay);
}
/**
* Binds all overlay interaction events. Call once during app initialization.
*/
export function init_table_overlay() {
div_overlay = getEl('id-table-overlay');
div_title = getEl('id-table-overlay-title');
div_view = content_of(getEl('id-table-overlay-view'));
input_filter = getEl('id-input-table-overlay-filter');
if (!div_overlay || !div_view || !input_filter) return;
addSafeEventListener(getEl('btn-table-overlay-close'), 'click', close_table_overlay);
addSafeEventListener(input_filter, 'input', () => {
table_params.filter = input_filter.value;
render_table();
});
}
/**
* Adds an expand button to every conversation table block in the container.
* The button opens the table overlay for that table.
* @param {HTMLElement} container - Container holding .div-table-block elements.
*/
export function attach_table_expand_buttons(container) {
if (!container) return;
for (const block of queryAll('.div-table-block', container)) {
if (block.querySelector('.div-table-expand-btn')) continue;
const header = queryEl('.div-table-block-header', block) || block;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'div-table-expand-btn as-icon';
btn.title = 'Expand Table';
btn.textContent = '🔍';
btn.onclick = () => {
let data = null;
try {
data = JSON.parse(block.getAttribute('data-table') || 'null');
} catch (e) {
data = null;
}
if (data) open_table_overlay(data);
};
header.appendChild(btn);
}
}
/**
* Renders the table from a filtered and sorted copy of the table JSON
* structure, then highlights the filter term in the visible cell text.
*/
function render_table() {
if (!table_data || !div_view || !div_title) return;
if (table_data.caption) {
div_title.innerHTML = table_data.caption;
div_title.style.display = 'block';
} else {
div_title.style.display = 'none';
}
const rows = table_data['table-rows'].map(row => Array.isArray(row) ? row.slice() : []);
const has_header = Boolean(table_data['has-header']) && rows.length > 0;
const header = has_header ? rows.shift() : [];
const filter = table_params.filter.trim().toLowerCase();
let body_rows = rows;
if (filter) {
body_rows = body_rows.filter(row => row.some(cell => get_cell_text(cell).toLowerCase().includes(filter)));
}
if (table_params.sort_col >= 0) {
sort_rows(body_rows, table_params.sort_col, table_params.sort_dir);
}
let html = '';
html += '<div class="div-table-overlay-tablewrap">';
html += '<table>';
if (has_header) {
html += '<thead><tr>';
header.forEach((title, col) => {
html += '<th data-col="' + col + '">' + title + get_sort_indicator(col) + '</th>';
});
html += '</tr></thead>';
}
html += '<tbody>';
for (const row of body_rows) {
html += '<tr>';
for (const cell of row) html += '<td>' + cell + '</td>';
html += '</tr>';
}
html += '</tbody></table>';
html += '</div>';
div_view.innerHTML = html;
if (filter) {
highlight_filter(queryEl('tbody', div_view), filter);
}
for (const th of queryAll('th[data-col]', div_view)) {
addSafeEventListener(th, 'click', () => on_header_click(parseInt(th.getAttribute('data-col'), 10)));
}
}
/**
* Returns the right-aligned sort indicator for a header column: an arrow when
* that column is sorted, a small dot otherwise.
* @param {number} col - The column index.
* @returns {string} The indicator HTML.
*/
function get_sort_indicator(col) {
if (table_params.sort_col !== col) {
return '<span class="span-th-sort span-th-sort-none">·</span>';
}
const arrow = (table_params.sort_dir === 'asc') ? '▴' : '▾';
return '<span class="span-th-sort">' + arrow + '</span>';
}
/**
* Handles a header click: cycles the sort column through ascending,
* descending, and no sorting, then re-renders.
* @param {number} col - The clicked column index.
*/
function on_header_click(col) {
if (isNaN(col)) return;
if (table_params.sort_col === col) {
if (table_params.sort_dir === 'asc') {
table_params.sort_dir = 'desc';
} else {
table_params.sort_col = -1;
table_params.sort_dir = 'asc';
}
} else {
table_params.sort_col = col;
table_params.sort_dir = 'asc';
}
render_table();
}
/**
* Sorts the rows in place by the text of the given column, numerically when
* both values are numeric, otherwise case-insensitively as text.
* @param {Array<Array>} rows - The rows to sort.
* @param {number} col - The column index to sort by.
* @param {string} dir - 'asc' or 'desc'.
*/
function sort_rows(rows, col, dir) {
const factor = (dir === 'desc') ? -1 : 1;
rows.sort((a, b) => {
const a_val = get_cell_text(a[col]).trim();
const b_val = get_cell_text(b[col]).trim();
const a_num = parseFloat(a_val);
const b_num = parseFloat(b_val);
let result;
if (!isNaN(a_num) && !isNaN(b_num) && a_val !== '' && b_val !== '') {
result = a_num - b_num;
} else {
result = a_val.toLowerCase().localeCompare(b_val.toLowerCase());
}
return result * factor;
});
}
/**
* Reads the visible text of a table cell, stripping any HTML markup.
* @param {*} cell - The raw cell value.
* @returns {string} The cell text.
*/
function get_cell_text(cell) {
if (cell === null || cell === undefined) return '';
const el = document.createElement('span');
el.innerHTML = String(cell);
return el.textContent || '';
}
/**
* Wraps every case-insensitive occurrence of the filter term in the text
* nodes of the given root with a <mark> element.
* @param {HTMLElement|null} root - The element to highlight within.
* @param {string} filter - The lowercased filter term.
*/
function highlight_filter(root, filter) {
if (!root || !filter) return;
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
const text_nodes = [];
while (walker.nextNode()) text_nodes.push(walker.currentNode);
for (const node of text_nodes) {
const text = node.nodeValue;
const lower = text.toLowerCase();
if (!lower.includes(filter)) continue;
const fragment = document.createDocumentFragment();
let pos = 0;
let idx = lower.indexOf(filter);
while (idx >= 0) {
if (idx > pos) fragment.appendChild(document.createTextNode(text.substring(pos, idx)));
const mark = document.createElement('mark');
mark.textContent = text.substring(idx, idx + filter.length);
fragment.appendChild(mark);
pos = idx + filter.length;
idx = lower.indexOf(filter, pos);
}
if (pos < text.length) fragment.appendChild(document.createTextNode(text.substring(pos)));
node.parentNode.replaceChild(fragment, node);
}
}
table-overlay.js
×
Type: Web, text/x-java
8.10 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File
Type: Web, text/x-java
8.10 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File