PageSourceSearch

https://jayreddinjr-jr-odysseus-hf.hf.space/static/js/editor/wire-topbar-menus.js

js jayreddinjr-jr-odysseus-hf.hf.space collected 2026-10-04 17:01:30 UTC 6,372 bytes, 174 lines download raw bytes

1/**
2 * Topbar dropdown menus — Image, Filter, and Resize.
3 *
4 *   Image menu (#ge-image-menu-btn → #ge-image-menu):
5 *     resize, selection (edge feather/delete), fill, rotate 90/180,
6 *     flip horizontal/vertical.
7 *
8 *   Filter menu (#ge-filter-menu-btn → #ge-filter-menu):
9 *     Blur sub-menu — Gaussian, Zoom.
10 *
11 *   Resize menu (#ge-resize-menu-btn → #ge-resize-menu):
12 *     preset W×H items (data-resize-w/-h) apply immediately;
13 *     [data-resize-custom] opens a themed prompt for arbitrary sizes.
14 *
15 * Returns the resize helpers so the keyboard-shortcuts module can
16 * call them too (Ctrl+Shift+T opens the custom prompt).
17 *
18 * @param {{
19 *   closeOtherTopbarMenus: (keepId: string) => void,
20 *   registerDocClickAway:  (handler: (e: Event) => void) => void,
21 *   saveState:             (label?: string) => void,
22 *   composite:             () => void,
23 *   fitZoom:               () => void,
24 *   promptCanvasSize:      (opts: object) => Promise<{w, h} | null>,
25 *   doFillSelection:       () => void,
26 *   rotateAllLayers:       (deg: number) => void,
27 *   flipAllLayers:         (axis: 'h' | 'v') => void,
28 *   applyGaussianBlur:     () => void,
29 *   applyZoomBlur:         () => void,
30 *   uiModule:              object,
31 * }} deps
32 *
33 * @returns {{
34 *   applyResize:         (newW: number, newH: number) => void,
35 *   resizeCustomPrompt:  () => Promise<void>,
36 * }}
37 */
38import { state } from './state.js';
39
40export function wireTopbarMenus({
41  closeOtherTopbarMenus, registerDocClickAway,
42  saveState, composite, fitZoom,
43  promptCanvasSize, doFillSelection,
44  rotateAllLayers, flipAllLayers,
45  applyGaussianBlur, applyZoomBlur,
46  uiModule,
47}) {
48  // ── Resize canvas ──
49  // Extracted so both the popup presets and the Ctrl+Shift+T shortcut
50  // can call it.
51  function applyResize(newW, newH) {
52    if (!newW || !newH || newW < 1 || newH < 1) {
53      uiModule.showToast('Invalid size');
54      return;
55    }
56    saveState('Resize canvas');
57    // Only resize the main canvas — layers keep their original size.
58    // Content outside the new bounds is clipped during composite, not
59    // destroyed.
60    if (state.maskCanvas) {
61      const tmpMask = document.createElement('canvas');
62      tmpMask.width = state.maskCanvas.width;
63      tmpMask.height = state.maskCanvas.height;
64      tmpMask.getContext('2d').drawImage(state.maskCanvas, 0, 0);
65      state.maskCanvas.width = newW;
66      state.maskCanvas.height = newH;
67      state.maskCtx.drawImage(tmpMask, 0, 0);
68    }
69    state.imgWidth = newW;
70    state.imgHeight = newH;
71    state.mainCanvas.width = newW;
72    state.mainCanvas.height = newH;
73    const sizeLabel = document.getElementById('ge-canvas-size');
74    if (sizeLabel) sizeLabel.textContent = `${newW}×${newH}`;
75    fitZoom();
76    composite();
77    uiModule.showToast(`Canvas resized to ${newW}×${newH}`);
78  }
79
80  async function resizeCustomPrompt() {
81    const result = await promptCanvasSize({
82      title: 'Canvas size',
83      okLabel: 'Apply',
84      initialW: state.imgWidth,
85      initialH: state.imgHeight,
86    });
87    if (!result) return;
88    applyResize(result.w, result.h);
89  }
90
91  // ── Image menu ──
92  {
93    const btn = document.getElementById('ge-image-menu-btn');
94    const menu = document.getElementById('ge-image-menu');
95    if (btn && menu) {
96      btn.addEventListener('click', (e) => {
97        e.stopPropagation();
98        const willOpen = menu.hidden;
99        if (willOpen) closeOtherTopbarMenus('ge-image-menu');
100        menu.hidden = !menu.hidden;
101      });
102      menu.addEventListener('click', (e) => {
103        const item = e.target.closest('[data-image-action]');
104        if (!item || item.disabled) return;
105        menu.hidden = true;
106        const action = item.dataset.imageAction;
107        if (action === 'resize') resizeCustomPrompt();
108        else if (action === 'selection') document.getElementById('ge-edge-menu-btn')?.click();
109        else if (action === 'fill') doFillSelection();
110        else if (action === 'rotate-90') rotateAllLayers(90);
111        else if (action === 'rotate-180') rotateAllLayers(180);
112        else if (action === 'flip-h') flipAllLayers('h');
113        else if (action === 'flip-v') flipAllLayers('v');
114      });
115      registerDocClickAway((e) => {
116        if (!menu.hidden && !menu.contains(e.target) && e.target !== btn) menu.hidden = true;
117      });
118    }
119  }
120
121  // ── Filter menu (Blur sub-menu — Gaussian / Zoom) ──
122  {
123    const btn = document.getElementById('ge-filter-menu-btn');
124    const menu = document.getElementById('ge-filter-menu');
125    if (btn && menu) {
126      btn.addEventListener('click', (e) => {
127        e.stopPropagation();
128        const willOpen = menu.hidden;
129        if (willOpen) closeOtherTopbarMenus('ge-filter-menu');
130        menu.hidden = !menu.hidden;
131      });
132      menu.addEventListener('click', (e) => {
133        const item = e.target.closest('[data-filter-action]');
134        if (!item) return;
135        menu.hidden = true;
136        const action = item.dataset.filterAction;
137        if (action === 'blur-gaussian') applyGaussianBlur();
138        else if (action === 'blur-zoom') applyZoomBlur();
139      });
140      registerDocClickAway((e) => {
141        if (!menu.hidden && !menu.contains(e.target) && e.target !== btn) menu.hidden = true;
142      });
143    }
144  }
145
146  // ── Resize popup (preset items + Custom… → resizeCustomPrompt) ──
147  {
148    const btn = document.getElementById('ge-resize-menu-btn');
149    const menu = document.getElementById('ge-resize-menu');
150    if (btn && menu) {
151      btn.addEventListener('click', (e) => {
152        e.stopPropagation();
153        const willOpen = menu.hidden;
154        if (willOpen) closeOtherTopbarMenus('ge-resize-menu');
155        menu.hidden = !menu.hidden;
156      });
157      menu.querySelectorAll('[data-resize-w]').forEach(item => {
158        item.addEventListener('click', () => {
159          menu.hidden = true;
160          applyResize(parseInt(item.dataset.resizeW, 10), parseInt(item.dataset.resizeH, 10));
161        });
162      });
163      menu.querySelector('[data-resize-custom]')?.addEventListener('click', () => {
164        menu.hidden = true;
165        resizeCustomPrompt();
166      });
167      registerDocClickAway((e) => {
168        if (!menu.hidden && !menu.contains(e.target) && e.target !== btn) menu.hidden = true;
169      });
170    }
171  }
172
173  return { applyResize, resizeCustomPrompt };
174}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.