@tobq/boardclip 2.1.111 → 2.1.113
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/window-placement.js +80 -0
- package/main.js +67 -9
- package/package.json +1 -1
- package/site/shared/clipboard-popup.css +10 -3
- package/site/shared/clipboard-ui-core.js +9 -8
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Where a clip window (editor, viewer, unify, conflict) opens, as pure maths so
|
|
4
|
+
// it is unit-tested (test/window-placement.test.js); main.js supplies the saved
|
|
5
|
+
// bounds, the display's work area, the clip windows already showing and the
|
|
6
|
+
// popup's bounds when the popup stays up. Owner 2026-10-13: "spawns under BC
|
|
7
|
+
// itself" / "multi spawning they dont stack, it jsut all sits on top".
|
|
8
|
+
// - A clip window already open at the spot pushes the new one a cascade step on,
|
|
9
|
+
// wrapping inside the work area, like the OS cascades documents.
|
|
10
|
+
// - With the popup up (keepPopup opens) the window goes beside the popup, never
|
|
11
|
+
// under it: the popup is always on top. It fans out away from the popup.
|
|
12
|
+
|
|
13
|
+
const CASCADE_STEP = 28;
|
|
14
|
+
const POPUP_GAP = 8;
|
|
15
|
+
function rectsOverlap(a, b) {
|
|
16
|
+
return a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
|
|
17
|
+
}
|
|
18
|
+
function clampInto(rect, area) {
|
|
19
|
+
return {
|
|
20
|
+
...rect,
|
|
21
|
+
x: Math.max(area.x, Math.min(rect.x, area.x + area.width - rect.width)),
|
|
22
|
+
y: Math.max(area.y, Math.min(rect.y, area.y + area.height - rect.height)),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
// Pure placement (unit-tested): base = the saved bounds (x/y may be missing),
|
|
26
|
+
// area = the work area to stay in, taken = top-left corners of the clip windows
|
|
27
|
+
// already showing, avoid = the open popup's bounds when it stays up.
|
|
28
|
+
function placeClipWindowBounds(base, { area, taken = [], avoid = null } = {}) {
|
|
29
|
+
const width = Math.min(base.width, area.width);
|
|
30
|
+
const height = Math.min(base.height, area.height);
|
|
31
|
+
let start = Number.isFinite(base.x) && Number.isFinite(base.y)
|
|
32
|
+
? clampInto({ x: base.x, y: base.y, width, height }, area)
|
|
33
|
+
: { x: Math.round(area.x + (area.width - width) / 2), y: Math.round(area.y + (area.height - height) / 2), width, height };
|
|
34
|
+
// Cascade direction: down and right, unless the window sits left of / above
|
|
35
|
+
// the popup, where it fans out away from the popup instead.
|
|
36
|
+
let dx = 1;
|
|
37
|
+
let dy = 1;
|
|
38
|
+
if (avoid && rectsOverlap(start, avoid)) {
|
|
39
|
+
const midY = avoid.y + Math.round((avoid.height - height) / 2);
|
|
40
|
+
const midX = avoid.x + Math.round((avoid.width - width) / 2);
|
|
41
|
+
const sides = [
|
|
42
|
+
{ x: avoid.x + avoid.width + POPUP_GAP, y: midY, dx: 1, dy: 1 },
|
|
43
|
+
{ x: avoid.x - POPUP_GAP - width, y: midY, dx: -1, dy: 1 },
|
|
44
|
+
{ x: midX, y: avoid.y + avoid.height + POPUP_GAP, dx: 1, dy: 1 },
|
|
45
|
+
{ x: midX, y: avoid.y - POPUP_GAP - height, dx: 1, dy: -1 },
|
|
46
|
+
].map((s) => ({ ...s, rect: clampInto({ x: s.x, y: s.y, width, height }, area) }));
|
|
47
|
+
const overlapArea = (r) => Math.max(0, Math.min(r.x + width, avoid.x + avoid.width) - Math.max(r.x, avoid.x))
|
|
48
|
+
* Math.max(0, Math.min(r.y + height, avoid.y + avoid.height) - Math.max(r.y, avoid.y));
|
|
49
|
+
const best = sides.find((s) => !rectsOverlap(s.rect, avoid))
|
|
50
|
+
|| sides.slice().sort((a, b) => overlapArea(a.rect) - overlapArea(b.rect))[0];
|
|
51
|
+
start = best.rect;
|
|
52
|
+
dx = best.dx;
|
|
53
|
+
dy = best.dy;
|
|
54
|
+
}
|
|
55
|
+
// The cascade walks diagonals (one step down and along per window); a
|
|
56
|
+
// diagonal that leaves the work area starts the next one a step further
|
|
57
|
+
// along at the area's top (or bottom, when fanning upward), like the OS
|
|
58
|
+
// cascade. The first spot no showing clip window holds wins.
|
|
59
|
+
const near = (p, q) => Math.abs(p.x - q.x) < 8 && Math.abs(p.y - q.y) < 8;
|
|
60
|
+
const fits = (p) => p.x >= area.x && p.y >= area.y && p.x + width <= area.x + area.width && p.y + height <= area.y + area.height;
|
|
61
|
+
// A wrapped spot never lands under the popup (unless no side had room for
|
|
62
|
+
// the window, when the least-covered spot is the best there is).
|
|
63
|
+
const besideFits = !(avoid && rectsOverlap(start, avoid));
|
|
64
|
+
const underPopup = (r) => !!avoid && besideFits && rectsOverlap(r, avoid);
|
|
65
|
+
const spanX = Math.max(1, area.width - width + 1);
|
|
66
|
+
const wrapX = (x) => area.x + ((((x - area.x) % spanX) + spanX) % spanX);
|
|
67
|
+
for (let column = 0; column < 64; column += 1) {
|
|
68
|
+
const x0 = wrapX(start.x + dx * column * CASCADE_STEP);
|
|
69
|
+
const y0 = column === 0 ? start.y : dy > 0 ? area.y : area.y + area.height - height;
|
|
70
|
+
for (let step = 0; step < 64; step += 1) {
|
|
71
|
+
const p = { x: x0 + dx * step * CASCADE_STEP, y: y0 + dy * step * CASCADE_STEP };
|
|
72
|
+
if (!fits(p)) break;
|
|
73
|
+
if (underPopup({ ...p, width, height })) continue;
|
|
74
|
+
if (!taken.some((t) => near(t, p))) return { x: p.x, y: p.y, width, height };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { x: start.x, y: start.y, width, height };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { placeClipWindowBounds, rectsOverlap, clampInto, CASCADE_STEP, POPUP_GAP };
|
package/main.js
CHANGED
|
@@ -14,6 +14,7 @@ const macPaste = require('./lib/macos-paste');
|
|
|
14
14
|
const winClipboard = require('./lib/windows-clipboard');
|
|
15
15
|
const { createQuickPaster } = require('./lib/quick-paste');
|
|
16
16
|
const { createClipboardFollower } = require('./lib/clipboard-follow');
|
|
17
|
+
const { placeClipWindowBounds } = require('./lib/window-placement');
|
|
17
18
|
const { dragFileNames } = require('./lib/drag-files');
|
|
18
19
|
const { createHistoryFeed } = require('./lib/history-feed');
|
|
19
20
|
// Shared UI core (pure UMD, no DOM at load): main reuses its chord tests.
|
|
@@ -4466,10 +4467,10 @@ function createPopup() {
|
|
|
4466
4467
|
setTimeout(() => {
|
|
4467
4468
|
if (Date.now() < ignoreBlurUntil || !isPopupOpen()) return;
|
|
4468
4469
|
if (!win.isFocused()) {
|
|
4469
|
-
// Focus moved to one of BoardClip's OWN windows
|
|
4470
|
-
//
|
|
4471
|
-
//
|
|
4472
|
-
// (getFocusedWindow() is null then)
|
|
4470
|
+
// Focus moved to one of BoardClip's OWN windows: a clip window's own
|
|
4471
|
+
// focus hook (trackClipWindow) closes the popup; any other (the AI
|
|
4472
|
+
// approval prompt) leaves it open. Focus leaving to another app
|
|
4473
|
+
// (getFocusedWindow() is null then) closes it here.
|
|
4473
4474
|
const focused = BrowserWindow.getFocusedWindow();
|
|
4474
4475
|
if (focused && focused !== win) return;
|
|
4475
4476
|
hidePopup();
|
|
@@ -5359,6 +5360,7 @@ function windowBoundsFromSettings(key, defaults) {
|
|
|
5359
5360
|
return out;
|
|
5360
5361
|
}
|
|
5361
5362
|
const saveBoundsTimers = new Map();
|
|
5363
|
+
const placedBounds = new WeakMap(); // clip window -> the bounds it was opened at
|
|
5362
5364
|
function scheduleWindowBoundsSave(childWin, key) {
|
|
5363
5365
|
if (!childWin || childWin.isDestroyed()) return;
|
|
5364
5366
|
const prior = saveBoundsTimers.get(key);
|
|
@@ -5368,6 +5370,14 @@ function scheduleWindowBoundsSave(childWin, key) {
|
|
|
5368
5370
|
if (!childWin || childWin.isDestroyed()) return;
|
|
5369
5371
|
const maximized = childWin.isMaximized() || childWin.isFullScreen();
|
|
5370
5372
|
const { x, y, width, height } = maximized ? childWin.getNormalBounds() : childWin.getBounds();
|
|
5373
|
+
// A window still where it was placed (a cascade step, beside the popup)
|
|
5374
|
+
// never becomes the saved spot: only a move or resize by the user does, so
|
|
5375
|
+
// untouched windows do not drift the next one's start.
|
|
5376
|
+
const placed = placedBounds.get(childWin);
|
|
5377
|
+
const savedNow = settings[key] || {};
|
|
5378
|
+
if (placed && placed.x === x && placed.y === y && placed.width === width && placed.height === height
|
|
5379
|
+
&& maximized === (savedNow.maximized === true)) return;
|
|
5380
|
+
placedBounds.delete(childWin);
|
|
5371
5381
|
settings[key] = { x, y, width: Math.max(360, width), height: Math.max(240, height), maximized };
|
|
5372
5382
|
saveSettingsFile({ localOnly: true });
|
|
5373
5383
|
}, 300);
|
|
@@ -5389,15 +5399,56 @@ function restoreWindowMaximized(childWin, key, options) {
|
|
|
5389
5399
|
if (!saved || saved.maximized !== true || (options && options.keepPopup)) return;
|
|
5390
5400
|
try { childWin.maximize(); } catch {}
|
|
5391
5401
|
}
|
|
5392
|
-
|
|
5402
|
+
// Every clip window (editor, viewer, unify, conflict) opens at its kind's saved
|
|
5403
|
+
// bounds, then cascades past the clip windows already showing and, when the
|
|
5404
|
+
// popup stays up (keepPopup), goes beside the popup instead of under it
|
|
5405
|
+
// (lib/window-placement.js). The work area is the popup's display, else the
|
|
5406
|
+
// saved spot's, else the cursor's.
|
|
5407
|
+
const clipWindows = new Set();
|
|
5408
|
+
// Going INTO a clip window (clicking it, or any other way it takes the focus)
|
|
5409
|
+
// closes the popup, on every platform: the "open several" gestures leave the
|
|
5410
|
+
// popup up only while the new windows open behind the scenes (showInactive).
|
|
5411
|
+
// Owner 2026-10-13: "still doesnt close going to clip. inconsisnte" - the
|
|
5412
|
+
// popup's blur handler kept it open whenever the focus went to one of our own
|
|
5413
|
+
// windows, and macOS has no blur-to-hide at all.
|
|
5414
|
+
let clipFocusHideHeldUntil = 0;
|
|
5415
|
+
function trackClipWindow(childWin) {
|
|
5416
|
+
clipWindows.add(childWin);
|
|
5417
|
+
try { placedBounds.set(childWin, childWin.getBounds()); } catch {}
|
|
5418
|
+
childWin.on('focus', () => {
|
|
5419
|
+
if (Date.now() < clipFocusHideHeldUntil || !isPopupOpen()) return;
|
|
5420
|
+
hidePopup();
|
|
5421
|
+
});
|
|
5422
|
+
childWin.once('closed', () => clipWindows.delete(childWin));
|
|
5423
|
+
}
|
|
5424
|
+
function clipWindowBoundsFromSettings(key, defaults, options) {
|
|
5425
|
+
const base = windowBoundsFromSettings(key, defaults);
|
|
5426
|
+
const keepPopup = !!(options && options.keepPopup) && isPopupOpen();
|
|
5427
|
+
const avoid = keepPopup && win && !win.isDestroyed() ? win.getBounds() : null;
|
|
5428
|
+
const anchor = avoid
|
|
5429
|
+
? { x: avoid.x + Math.round(avoid.width / 2), y: avoid.y + Math.round(avoid.height / 2) }
|
|
5430
|
+
: Number.isFinite(base.x) ? { x: base.x + Math.round(base.width / 2), y: base.y + Math.round(base.height / 2) }
|
|
5431
|
+
: screen.getCursorScreenPoint();
|
|
5432
|
+
const area = screen.getDisplayNearestPoint(anchor).workArea;
|
|
5433
|
+
const taken = [];
|
|
5434
|
+
for (const other of clipWindows) {
|
|
5435
|
+
try {
|
|
5436
|
+
if (other.isDestroyed() || !other.isVisible() || other.isMinimized() || other.isMaximized()) continue;
|
|
5437
|
+
const b = other.getBounds();
|
|
5438
|
+
taken.push({ x: b.x, y: b.y });
|
|
5439
|
+
} catch {}
|
|
5440
|
+
}
|
|
5441
|
+
return placeClipWindowBounds(base, { area, taken, avoid });
|
|
5442
|
+
}
|
|
5443
|
+
function editorBoundsFromSettings(options) { return clipWindowBoundsFromSettings('editor_bounds', { width: 560, height: 520 }, options); }
|
|
5393
5444
|
// The merge windows (unify, conflict) keep their own size: two or three panes
|
|
5394
5445
|
// with a head each need width a plain editor does not (and a widened unify
|
|
5395
5446
|
// must not leave the next editor wide).
|
|
5396
|
-
function mergeBoundsFromSettings() { return
|
|
5447
|
+
function mergeBoundsFromSettings(options) { return clipWindowBoundsFromSettings('merge_bounds', { width: 780, height: 560 }, options); }
|
|
5397
5448
|
|
|
5398
5449
|
function createEditorWindow(session, presentOptions = {}) {
|
|
5399
5450
|
const editorWin = new BrowserWindow({
|
|
5400
|
-
...editorBoundsFromSettings(),
|
|
5451
|
+
...editorBoundsFromSettings(presentOptions),
|
|
5401
5452
|
minWidth: 360,
|
|
5402
5453
|
minHeight: 240,
|
|
5403
5454
|
resizable: true,
|
|
@@ -5414,6 +5465,7 @@ function createEditorWindow(session, presentOptions = {}) {
|
|
|
5414
5465
|
},
|
|
5415
5466
|
});
|
|
5416
5467
|
noteSecondarySurface(editorWin);
|
|
5468
|
+
trackClipWindow(editorWin);
|
|
5417
5469
|
session.win = editorWin;
|
|
5418
5470
|
editorWin.loadFile(path.join(SCRIPT_DIR, 'editor.html'));
|
|
5419
5471
|
// Show + focus the editor, then dismiss the popup so it doesn't linger behind
|
|
@@ -5477,13 +5529,16 @@ function sourceGroupsForNewNote(options) {
|
|
|
5477
5529
|
// Bring an editor/viewer window on screen. Default = a deliberate hand-off: the
|
|
5478
5530
|
// window takes focus and the popup is dismissed (a popup lingering behind the
|
|
5479
5531
|
// editor felt "sticky" - you had to click back to it + Escape). With
|
|
5480
|
-
// options.keepPopup (
|
|
5532
|
+
// options.keepPopup (the "open several" gestures: middle-click, right-click on the open button, alt+click)
|
|
5481
5533
|
// the window is shown WITHOUT activating and the popup stays where it is, so
|
|
5482
5534
|
// several results can be opened one after another; the popup still hides on
|
|
5483
5535
|
// its own blur the moment the user clicks into one of the windows.
|
|
5484
5536
|
function presentSecondaryWindow(targetWin, options = {}) {
|
|
5485
5537
|
if (!targetWin || targetWin.isDestroyed()) return;
|
|
5486
5538
|
if (options && options.keepPopup) {
|
|
5539
|
+
// Backstop: a focus event the inactive show raises on its own must not
|
|
5540
|
+
// close the popup the user is still opening clips from.
|
|
5541
|
+
clipFocusHideHeldUntil = Date.now() + 300;
|
|
5487
5542
|
targetWin.showInactive();
|
|
5488
5543
|
if (typeof targetWin.moveTop === 'function') { try { targetWin.moveTop(); } catch {} }
|
|
5489
5544
|
return;
|
|
@@ -5648,7 +5703,7 @@ function openImageViewer(id, options = {}) {
|
|
|
5648
5703
|
return;
|
|
5649
5704
|
}
|
|
5650
5705
|
const viewerWin = new BrowserWindow({
|
|
5651
|
-
...
|
|
5706
|
+
...clipWindowBoundsFromSettings('viewer_bounds', { width: 720, height: 560 }, options),
|
|
5652
5707
|
minWidth: 360,
|
|
5653
5708
|
minHeight: 240,
|
|
5654
5709
|
resizable: true,
|
|
@@ -5665,6 +5720,7 @@ function openImageViewer(id, options = {}) {
|
|
|
5665
5720
|
},
|
|
5666
5721
|
});
|
|
5667
5722
|
noteSecondarySurface(viewerWin);
|
|
5723
|
+
trackClipWindow(viewerWin);
|
|
5668
5724
|
viewerWindows.set(key, viewerWin);
|
|
5669
5725
|
viewerWin.loadFile(path.join(SCRIPT_DIR, 'viewer.html'));
|
|
5670
5726
|
viewerWin.once('ready-to-show', () => {
|
|
@@ -5821,6 +5877,7 @@ function openConflictWindow(conflictId) {
|
|
|
5821
5877
|
},
|
|
5822
5878
|
});
|
|
5823
5879
|
noteSecondarySurface(conflictWin);
|
|
5880
|
+
trackClipWindow(conflictWin);
|
|
5824
5881
|
conflictWindows.set(id, conflictWin);
|
|
5825
5882
|
const sessionId = `conflict:${id}`;
|
|
5826
5883
|
conflictWin.loadFile(path.join(SCRIPT_DIR, 'editor.html'));
|
|
@@ -5938,6 +5995,7 @@ function openUnifyWindow(session) {
|
|
|
5938
5995
|
},
|
|
5939
5996
|
});
|
|
5940
5997
|
noteSecondarySurface(unifyWin);
|
|
5998
|
+
trackClipWindow(unifyWin);
|
|
5941
5999
|
session.win = unifyWin;
|
|
5942
6000
|
unifyWin.loadFile(path.join(SCRIPT_DIR, 'editor.html'));
|
|
5943
6001
|
unifyWin.once('ready-to-show', () => {
|
package/package.json
CHANGED
|
@@ -316,7 +316,13 @@
|
|
|
316
316
|
display: inline-flex; align-items: center; gap: var(--sp-1); vertical-align: top; font-family: inherit; }
|
|
317
317
|
.filter-tag.icon-filter { min-width: var(--ctl-sm); height: var(--ctl-sm); justify-content: center; padding: 0 var(--sp-2); }
|
|
318
318
|
.filter-tag:hover { background: var(--hover); color: var(--text); }
|
|
319
|
-
|
|
319
|
+
/* A group name reads in the accent text shade wherever it shows: its chip, a
|
|
320
|
+
clip's meta line and window bar, every group picker and chip dropdown row,
|
|
321
|
+
Settings > Groups (owner 2026-10-09 on the chips: "too hard to see before
|
|
322
|
+
selected"; 2026-10-13: "blue tags should be consistent everywhere"). Hover
|
|
323
|
+
keeps the colour (the wash says hover); a picked chip's fill and an excluded
|
|
324
|
+
name's strike override it. */
|
|
325
|
+
.filter-tag.group-tag, .filter-tag.group-tag:hover, .meta-tags > .meta-tag, .bc-menu-item.group-name > .bc-menu-label, .group-row .si-name { color: var(--accent-text); }
|
|
320
326
|
.filter-tag.active, .filter-tag.active:hover { background: var(--accent); color: var(--active-fg); --icon-fill: 1; }
|
|
321
327
|
.filter-tag.active:hover { background: var(--accent-hover); }
|
|
322
328
|
.filter-tag.excluded { color: var(--text-dim); text-decoration: line-through; text-decoration-thickness: 1px; opacity: .6; }
|
|
@@ -414,7 +420,7 @@
|
|
|
414
420
|
/* A group chip's dropdown holds menu rows (renderTagTreeMenuNode, depth > 0):
|
|
415
421
|
an excluded group is struck through like its chip, a greyed one dims in the
|
|
416
422
|
chips' two tiers, and a parent row stays lit while its submenu is out. */
|
|
417
|
-
.bc-menu-item.excluded > .bc-menu-label { text-decoration: line-through; }
|
|
423
|
+
.bc-menu-item.excluded > .bc-menu-label { color: var(--text-dim); text-decoration: line-through; }
|
|
418
424
|
.bc-menu-item.is-disabled { opacity: .4; cursor: default; }
|
|
419
425
|
.bc-menu-item.is-disabled.dis-structural { opacity: .25; }
|
|
420
426
|
.bc-menu-item.is-disabled:hover { background: none; }
|
|
@@ -494,7 +500,8 @@
|
|
|
494
500
|
at the last name and the + follows it instead of the box's far edge. */
|
|
495
501
|
.item:hover .meta-tag.meta-cut, .item:focus-within .meta-tag.meta-cut, .item.actions-held .meta-tag.meta-cut { display: none; }
|
|
496
502
|
.meta-tag { display: block; flex: 0 1 auto; min-width: 0; max-width: 140px; overflow: hidden; text-overflow: ellipsis; }
|
|
497
|
-
.meta-np:hover, .meta-
|
|
503
|
+
.meta-np:hover, .meta-ghost:hover { color: var(--text); }
|
|
504
|
+
.meta-np:hover, .meta-tag:hover, .meta-ghost:hover { background: var(--hover); }
|
|
498
505
|
.meta-ghost { display: inline-flex; align-items: center; justify-content: center; width: var(--meta-h); padding: 0; }
|
|
499
506
|
.meta-ghost .mi { font-size: var(--icon-sm); }
|
|
500
507
|
.meta > .meta-reveal, .bc-tag-strip > .meta-reveal { margin-left: calc(-1 * var(--sp-3)); --reveal-gap: var(--sp-3); }
|
|
@@ -220,7 +220,7 @@
|
|
|
220
220
|
check: state === 'include',
|
|
221
221
|
label: node.label || group,
|
|
222
222
|
hint: disabled ? '' : String(count),
|
|
223
|
-
cls: `group-filter-row${state === 'exclude' ? ' excluded' : ''}${disabled ? ` is-disabled${v.kind === 'structural' ? ' dis-structural' : ''}` : ''}`,
|
|
223
|
+
cls: `group-filter-row group-name${state === 'exclude' ? ' excluded' : ''}${disabled ? ` is-disabled${v.kind === 'structural' ? ' dis-structural' : ''}` : ''}`,
|
|
224
224
|
attrs: { 'data-group': group, title, 'aria-disabled': disabled ? 'true' : null },
|
|
225
225
|
};
|
|
226
226
|
return hasChildren
|
|
@@ -1166,7 +1166,7 @@
|
|
|
1166
1166
|
const group = normalizeTagName(node && node.name);
|
|
1167
1167
|
if (!group) return '';
|
|
1168
1168
|
const [icon, checked] = MEMBERSHIP_GLYPH[state(group)] || MEMBERSHIP_GLYPH.none;
|
|
1169
|
-
const row = { icon, check: true, label: node.label || group, role: 'menuitemcheckbox', attrs: { 'data-action': action, 'data-group': group, 'aria-checked': checked, title: group } };
|
|
1169
|
+
const row = { icon, check: true, label: node.label || group, cls: 'group-name', role: 'menuitemcheckbox', attrs: { 'data-action': action, 'data-group': group, 'aria-checked': checked, title: group } };
|
|
1170
1170
|
return node.children && node.children.length
|
|
1171
1171
|
? submenuNodeHtml(row, renderGroupChecklist(node.children, state, action))
|
|
1172
1172
|
: menuRowHtml(row);
|
|
@@ -4986,18 +4986,19 @@
|
|
|
4986
4986
|
refresh();
|
|
4987
4987
|
return true;
|
|
4988
4988
|
}
|
|
4989
|
-
//
|
|
4990
|
-
//
|
|
4991
|
-
//
|
|
4992
|
-
|
|
4989
|
+
// A plain click on any Open (the row's own open button or the clip menu's
|
|
4990
|
+
// Open in editor / Open image) is a hand-off: the window takes the focus
|
|
4991
|
+
// and the popup closes. Only the "open several" gestures keep the popup:
|
|
4992
|
+
// middle-click, right-click on the open button, Alt+click (owner
|
|
4993
|
+
// 2026-10-13: "when i click on note, bc doesnt close... inconsisntent").
|
|
4993
4994
|
const openImg = t.closest('[data-action="open-img"]');
|
|
4994
|
-
if (openImg) { event.stopPropagation(); await a.openImage(a.itemById(openImg.dataset.id)
|
|
4995
|
+
if (openImg) { event.stopPropagation(); await a.openImage(a.itemById(openImg.dataset.id)); return true; }
|
|
4995
4996
|
const openImgExt = t.closest('[data-action="open-img-ext"]');
|
|
4996
4997
|
if (openImgExt) { event.stopPropagation(); if (a.openImageExternal) await a.openImageExternal(a.itemById(openImgExt.dataset.id)); return true; }
|
|
4997
4998
|
const saveImg = t.closest('[data-action="save-img"]');
|
|
4998
4999
|
if (saveImg) { event.stopPropagation(); toast(await a.saveImage(a.itemById(saveImg.dataset.id))); return true; }
|
|
4999
5000
|
const edit = t.closest('[data-action="edit"]');
|
|
5000
|
-
if (edit) { event.stopPropagation(); await a.editClip(edit.dataset.id, edit.closest('.item')
|
|
5001
|
+
if (edit) { event.stopPropagation(); await a.editClip(edit.dataset.id, edit.closest('.item')); return true; }
|
|
5001
5002
|
const rename = t.closest('[data-action="rename"]');
|
|
5002
5003
|
if (rename) { event.stopPropagation(); await renameClip(rename.dataset.id); return true; }
|
|
5003
5004
|
const revert = t.closest('[data-action="revert"]');
|