The page's three rows (kind pills, search, tag select, count and close; view, share and save; the drag tip strip) become one bar: search, a Filter menu (kind and tag), the count, grid or list, share and import, save the canvas. Cards keep the picture, the name, a line of steps, sampler, LoRAs and size, and the model; the models' state is a dot (words only when some are missing), 功能模块 marks a partial workflow, and Open and ⋯ (edit, export, delete) show on hover. ui_popup_menu.js is the small menu both use. The page's styles move to styles/28-workflows.css; the card and bar rules spread over four stylesheets and the ~240 lines of CSS injected from ui_recipe_cards.js are removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
79 lines
3.0 KiB
JavaScript
79 lines
3.0 KiB
JavaScript
/**
|
|
* A small menu that opens under a button (the workflow card's ⋯, the Workflows page's
|
|
* filter): rows that run an action, rows ticked as chosen, and headings. One is open at a
|
|
* time; a press outside it, Esc, scrolling or a chosen row closes it.
|
|
*/
|
|
|
|
let openMenu = null;
|
|
|
|
function el(tag, className, text) {
|
|
const node = document.createElement(tag);
|
|
if (className) node.className = className;
|
|
if (text !== undefined) node.textContent = text;
|
|
return node;
|
|
}
|
|
|
|
/** Closes the open menu, if any. */
|
|
export function closePopupMenu() {
|
|
openMenu?.();
|
|
}
|
|
|
|
/**
|
|
* Opens `items` under `anchor`: { heading } or { label, run, checked?, danger? }. A row with
|
|
* `checked` (true or false) shows a tick when true. Pressing the anchor again closes it.
|
|
*/
|
|
export function openPopupMenu(anchor, items) {
|
|
const wasOpenHere = openMenu?.anchor === anchor;
|
|
closePopupMenu();
|
|
if (wasOpenHere) return;
|
|
const menu = el('div', 'anomalous-popup-menu');
|
|
menu.setAttribute('role', 'menu');
|
|
for (const item of items) {
|
|
if (item.heading) {
|
|
menu.append(el('div', 'anomalous-popup-menu-heading', item.heading));
|
|
continue;
|
|
}
|
|
const row = el('button', `anomalous-popup-menu-item${item.danger ? ' is-danger' : ''}`);
|
|
row.type = 'button';
|
|
row.setAttribute('role', item.checked === undefined ? 'menuitem' : 'menuitemradio');
|
|
if (item.checked !== undefined) {
|
|
row.setAttribute('aria-checked', String(Boolean(item.checked)));
|
|
row.append(el('span', 'anomalous-popup-menu-tick', item.checked ? '✓' : ''));
|
|
}
|
|
row.append(el('span', 'anomalous-popup-menu-label', item.label));
|
|
row.onclick = (event) => {
|
|
event.stopPropagation();
|
|
close();
|
|
item.run?.();
|
|
};
|
|
menu.append(row);
|
|
}
|
|
document.body.append(menu);
|
|
const box = anchor.getBoundingClientRect();
|
|
const { width, height } = menu.getBoundingClientRect();
|
|
const left = Math.min(Math.max(8, box.right - width), window.innerWidth - width - 8);
|
|
const below = box.bottom + 4;
|
|
menu.style.left = `${left}px`;
|
|
menu.style.top = `${below + height > window.innerHeight - 8 ? Math.max(8, box.top - height - 4) : below}px`;
|
|
|
|
const onPointer = (event) => {
|
|
if (!menu.contains(event.target) && !anchor.contains(event.target)) close();
|
|
};
|
|
const onKey = (event) => {
|
|
if (event.key !== 'Escape') return;
|
|
event.stopPropagation();
|
|
close();
|
|
};
|
|
const close = () => {
|
|
menu.remove();
|
|
window.removeEventListener('pointerdown', onPointer, true);
|
|
window.removeEventListener('keydown', onKey, true);
|
|
window.removeEventListener('scroll', close, true);
|
|
if (openMenu === close) openMenu = null;
|
|
};
|
|
close.anchor = anchor;
|
|
window.addEventListener('pointerdown', onPointer, true);
|
|
window.addEventListener('keydown', onKey, true);
|
|
window.addEventListener('scroll', close, true);
|
|
openMenu = close;
|
|
} |