Files
odysseus/static/js/editor/wire-topbar-overflow.js
T
pewdiepie-archdaemonandClaude Opus 5.5 5fac693c1c feat(photo-editor): Ajax Image Editor client (Photoshop-parity tools)
Photoshop-parity editor client: layers, selection/masking, brushes,
smart filters, LUT import, Quick Edit, mobile gestures, workspaces,
batch operations, and the service-worker versioning (v978->v1006).

Source: vendor/photocraft (ad1cff44) photo-editor client files, merged
onto upstream/dev (a8c147b2). Conflict resolution documented in
pr_work/A_conflicts.md; editor files taken from vendor/photocraft
(upstream's editor is an earlier snapshot of the same feature).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:57:35 +00:00

101 lines
3.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Topbar overflow handler — keeps lightweight labels updated and hides
* only low-priority AI model controls when the editor window gets narrow.
*
* Plus the small canvas-size display label updater (since it sits in
* the topbar too).
*
* Import and Canvas stay as real topbar buttons; there is intentionally
* no "More" overflow menu here.
*
* @param {{
* container: HTMLElement,
* registerDocClickAway: (handler: (e: Event) => void) => void,
* }} deps
*/
import { state } from './state.js';
let disposeTopbar;
export function wireTopbarOverflow({ container }) {
disposeTopbar?.();
const cleanups = [];
disposeTopbar = () => cleanups.forEach(cleanup => cleanup());
// Canvas-size badge updater (kept simple — it lives in the topbar).
const sizeLabel = document.getElementById('ge-canvas-size');
function updateSizeLabel() {
if (sizeLabel) sizeLabel.textContent = `${state.imgWidth}×${state.imgHeight} px`;
}
updateSizeLabel();
const topbar = container.querySelector('.ge-topbar');
// The Gen control + its "Gen" label span — collapse as a group when
// narrow. The Inpaint model selector moved into the side panel.
const aiGroup = [
container.querySelector('#ge-ai-model'),
...container.querySelectorAll('.ge-topbar span[style*="font-size:9px"]'),
].filter(Boolean);
// Native popovers keep the existing DOM/event ownership while escaping
// the toolbar's horizontal scroll clip and the editor's stacking context.
topbar?.querySelectorAll('.dropdown[hidden]').forEach(menu => {
if (!menu.showPopover) return;
const anchor = menu.parentElement.querySelector('button');
if (!anchor) return;
menu.setAttribute('popover', 'manual');
const position = () => {
const rect = anchor.getBoundingClientRect();
menu.style.top = `${rect.bottom + 4}px`;
// Anchor the menu's LEFT edge under the button's left edge
// (Photoshop behaviour), then clamp so the whole menu stays inside
// the viewport — a right-hand menu would otherwise overflow on the
// right, and a left-hand button (e.g. SELECT at 390px) would push
// a ~224px menu past the left edge.
const margin = 8;
const width = menu.getBoundingClientRect().width || menu.offsetWidth || 0;
let left = rect.left;
if (left + width > window.innerWidth - margin) {
left = window.innerWidth - margin - width;
}
if (left < margin) left = margin;
menu.style.left = `${left}px`;
menu.style.right = 'auto';
};
const sync = () => {
if (menu.hidden) {
if (menu.matches(':popover-open')) menu.hidePopover();
} else if (menu.isConnected) {
position();
if (!menu.matches(':popover-open')) menu.showPopover();
}
};
const observer = new MutationObserver(sync);
observer.observe(menu, { attributes: true, attributeFilter: ['hidden'] });
topbar.addEventListener('scroll', position, { passive: true });
window.addEventListener('resize', position);
cleanups.push(() => {
observer.disconnect();
topbar.removeEventListener('scroll', position);
window.removeEventListener('resize', position);
if (menu.matches(':popover-open')) menu.hidePopover();
});
});
function syncOverflow() {
if (!topbar) return;
aiGroup.forEach(el => { el.style.display = ''; });
if (topbar.scrollWidth > topbar.clientWidth) {
// Hide AI group first — bulky and least essential at narrow widths.
aiGroup.forEach(el => { el.style.display = 'none'; });
}
}
if (topbar && window.ResizeObserver) {
const ro = new ResizeObserver(() => syncOverflow());
ro.observe(topbar);
cleanups.push(() => ro.disconnect());
}
// Initial pass after layout settles.
requestAnimationFrame(syncOverflow);
}