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.