- Marker: "make a repaint mask" becomes How to change it (leave it to the AI, only the marked areas, the whole picture anew); the AI reads the choice first as the user's, the timeline row says it, the repaint brush picks "only the marked areas". - Task list: folded, it stays laid out out of sight and slides in at once from its button or the page's left edge; redraws make anew only the rows that changed (sync_rows.js). A picked task shows at once from the copy read before or read ahead on hover (ai_task_cache.js), then what changed; activity entries follow instead of holding it up. Stage pictures load as thumbnails first, the file itself only when shown larger. - Header: "Own tab" is "AI edits a copy ▾", a two-way menu with what each means (kept open across redraws), and "Show the copy". - Materials: the Source list lights the hovered row and ticks the one shown (a later rule had hidden both), tooltips only for cut names, arrows and Enter (ui_ai_facet_menu.js). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
89 lines
3.9 KiB
JavaScript
89 lines
3.9 KiB
JavaScript
/**
|
|
* The list of sources (or types) that the materials picker's side panel opens from its
|
|
* "Source: All ▾" button (ui_ai_toolbox.js), over the cards: the rows it is given, a search over
|
|
* their names when there are many, ↑ ↓ through the rows (from the search too) and Enter to pick.
|
|
* The row under the pointer or in focus lights up and the one shown is ticked (43-ai-toolbox.css).
|
|
* A pick, Esc or a press outside the list closes it; each listener goes with the list.
|
|
*/
|
|
|
|
import { translate as t } from './locales.js';
|
|
|
|
const SEARCH_FROM = 8;
|
|
|
|
let closeOpen = null; // closes the list on screen
|
|
|
|
function el(tag, className) {
|
|
const node = document.createElement(tag);
|
|
if (className) node.className = className;
|
|
return node;
|
|
}
|
|
|
|
export function closeFacetMenu() {
|
|
closeOpen?.();
|
|
}
|
|
|
|
/**
|
|
* Opens the list in `host` at `top` px (or closes it when it is open). `rows`: the row buttons
|
|
* (each with `dataset.label`, the one shown with class is-on); `title` names them for the search;
|
|
* `button` is what opened it (a press on it is not "outside").
|
|
*/
|
|
export function toggleFacetMenu({ host, top, title, rows, button }) {
|
|
if (closeOpen) return void closeFacetMenu();
|
|
const menu = el('div', 'anomalous-tb-facet-menu');
|
|
menu.dataset.anomalousEscape = '1'; // Esc closes this list, not the browser
|
|
const list = el('div', 'anomalous-tb-facet-menu-list');
|
|
list.setAttribute('role', 'listbox');
|
|
list.append(...rows);
|
|
let find = null;
|
|
if (rows.length > SEARCH_FROM) {
|
|
find = el('input', 'anomalous-tb-facet-menu-find');
|
|
find.type = 'search';
|
|
find.placeholder = t('aiTbFindSource', { what: title });
|
|
find.addEventListener('input', () => {
|
|
const words = find.value.trim().toLowerCase();
|
|
for (const row of rows) row.hidden = Boolean(words) && !String(row.dataset.label || '').toLowerCase().includes(words);
|
|
});
|
|
menu.append(find);
|
|
}
|
|
menu.append(list);
|
|
|
|
// ↑ ↓ go through the rows the search leaves; up from the first goes back to the search.
|
|
const shown = () => rows.filter(row => !row.hidden);
|
|
menu.addEventListener('keydown', (event) => {
|
|
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
event.preventDefault();
|
|
const items = shown();
|
|
const at = items.indexOf(document.activeElement);
|
|
if (event.key === 'ArrowUp' && at <= 0 && find) return void find.focus();
|
|
const next = items[at < 0 ? 0 : Math.max(0, Math.min(items.length - 1, at + (event.key === 'ArrowDown' ? 1 : -1)))];
|
|
next?.focus();
|
|
next?.scrollIntoView({ block: 'nearest' });
|
|
} else if (event.key === 'Enter' && event.target === find) {
|
|
event.preventDefault();
|
|
shown()[0]?.click();
|
|
}
|
|
});
|
|
menu.addEventListener('click', (event) => { if (event.target.closest('.anomalous-tb-facet')) close(); });
|
|
menu.style.top = `${top}px`;
|
|
menu.style.maxHeight = `${Math.max(160, host.clientHeight - top - 8)}px`;
|
|
host.append(menu);
|
|
requestAnimationFrame(() => {
|
|
const current = rows.find(row => row.classList.contains('is-on'));
|
|
(find || current || rows[0])?.focus({ preventScroll: true });
|
|
current?.scrollIntoView({ block: 'nearest' });
|
|
});
|
|
|
|
const outside = (event) => { if (!menu.contains(event.target) && !button?.contains(event.target)) close(); };
|
|
// After the browser's own Esc handler has seen the list's mark (it then keeps the browser open).
|
|
const escape = (event) => { if (event.key === 'Escape') setTimeout(close); };
|
|
setTimeout(() => { if (closeOpen === close) document.addEventListener('pointerdown', outside, true); });
|
|
document.addEventListener('keydown', escape);
|
|
function close() {
|
|
menu.remove();
|
|
document.removeEventListener('pointerdown', outside, true);
|
|
document.removeEventListener('keydown', escape);
|
|
if (closeOpen === close) closeOpen = null;
|
|
}
|
|
closeOpen = close;
|
|
}
|