@tobq/boardclip 2.1.108 → 2.1.109
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/index.html +5 -1
- package/main.js +23 -8
- package/package.json +1 -1
- package/preload.js +2 -1
- package/site/index.html +11 -7
- package/site/shared/clipboard-ui-core.js +32 -5
package/index.html
CHANGED
|
@@ -385,7 +385,8 @@ const appAdapter = {
|
|
|
385
385
|
// Always the id-based path: window.api.copy() only takes a string, so it
|
|
386
386
|
// would silently drop the clip's html/rtf. paste(id) puts every captured
|
|
387
387
|
// format on the clipboard for both text and image clips.
|
|
388
|
-
|
|
388
|
+
copyClip: (id) => window.api.copyClip(id),
|
|
389
|
+
copyMany: (ids) => window.api.copyMany(ids),
|
|
389
390
|
activateClip: (id) => window.api.pasteAndHide(id),
|
|
390
391
|
editClip: (id, _itemEl, options) => window.api.openEditor(id, { ...Core.editorFindFor(query), ...(options || {}) }),
|
|
391
392
|
setClipTitle: (id, title) => revGuard(() => window.api.setClipTitle(id, title, clipRevOf(id)), false),
|
|
@@ -491,6 +492,9 @@ document.getElementById('copyDiagnostics').addEventListener('click', async () =>
|
|
|
491
492
|
|
|
492
493
|
// --- Search ---
|
|
493
494
|
searchEl.addEventListener('keydown', e => {
|
|
495
|
+
// A digit typed while a menu or popover is open belongs to it, never a
|
|
496
|
+
// quick-paste (typing "1ndex" with the + picker open pasted slot 1).
|
|
497
|
+
if (controller.isMenuOpen()) return;
|
|
494
498
|
if (query === '' && !e.altKey && !e.ctrlKey && !e.metaKey && e.key >= '1' && e.key <= '9') {
|
|
495
499
|
const n = +e.key;
|
|
496
500
|
const nmap = numpadMap();
|
package/main.js
CHANGED
|
@@ -4889,9 +4889,9 @@ async function pasteAndHide(id) {
|
|
|
4889
4889
|
}
|
|
4890
4890
|
}
|
|
4891
4891
|
|
|
4892
|
-
// "Paste all":
|
|
4893
|
-
// one go. Images can't concatenate into text, so they're skipped.
|
|
4894
|
-
|
|
4892
|
+
// "Paste all" / "Copy all": the selected TEXT clips joined (newline-separated)
|
|
4893
|
+
// in one go. Images can't concatenate into text, so they're skipped.
|
|
4894
|
+
function joinedClipText(ids) {
|
|
4895
4895
|
const parts = [];
|
|
4896
4896
|
for (const id of (Array.isArray(ids) ? ids : [])) {
|
|
4897
4897
|
const item = findHistoryItem(id);
|
|
@@ -4899,18 +4899,30 @@ async function pasteMany(ids) {
|
|
|
4899
4899
|
textBlobStore.hydrateTextItem(item, TEXT_DIR);
|
|
4900
4900
|
parts.push(item.text || '');
|
|
4901
4901
|
}
|
|
4902
|
-
|
|
4902
|
+
return parts.join('\n');
|
|
4903
|
+
}
|
|
4904
|
+
// The joined text onto the clipboard, never captured back as a new clip.
|
|
4905
|
+
function writeJoinedText(text) {
|
|
4906
|
+
clipboard.writeText(text);
|
|
4907
|
+
lastText = text;
|
|
4908
|
+
lastImgHash = '';
|
|
4909
|
+
}
|
|
4910
|
+
async function pasteMany(ids) {
|
|
4911
|
+
const text = joinedClipText(ids);
|
|
4903
4912
|
if (!text) { hidePopup(); return; }
|
|
4904
4913
|
pollGate = false;
|
|
4905
4914
|
try {
|
|
4906
|
-
|
|
4907
|
-
lastText = text; // don't re-capture our own joined paste as a new clip
|
|
4908
|
-
lastImgHash = '';
|
|
4915
|
+
writeJoinedText(text);
|
|
4909
4916
|
await hideAndPasteForeground();
|
|
4910
4917
|
} finally {
|
|
4911
4918
|
pollGate = true;
|
|
4912
4919
|
}
|
|
4913
4920
|
}
|
|
4921
|
+
// "Copy all": the same text, nothing pasted; the popup stays.
|
|
4922
|
+
function copyMany(ids) {
|
|
4923
|
+
const text = joinedClipText(ids);
|
|
4924
|
+
if (text) writeJoinedText(text);
|
|
4925
|
+
}
|
|
4914
4926
|
|
|
4915
4927
|
function validNumpadSlot(slot) {
|
|
4916
4928
|
const slotNum = Number(slot);
|
|
@@ -6739,7 +6751,9 @@ function setupIPC() {
|
|
|
6739
6751
|
},
|
|
6740
6752
|
}));
|
|
6741
6753
|
|
|
6742
|
-
|
|
6754
|
+
// Copy without pasting: the clip onto the clipboard (the poller then sees it
|
|
6755
|
+
// like any copy, so it moves to the top as a paste does).
|
|
6756
|
+
ipcMain.handle('copy-clip', (_, id) => {
|
|
6743
6757
|
const item = findHistoryItem(id);
|
|
6744
6758
|
if (!item) return;
|
|
6745
6759
|
setClipboardToItem(item);
|
|
@@ -6966,6 +6980,7 @@ function setupIPC() {
|
|
|
6966
6980
|
ipcMain.handle('restore-items', (_, snaps) => applyRestoreItems(snaps));
|
|
6967
6981
|
ipcMain.handle('group-assign-many', (_, targets, group, shouldHave) => applyGroupAssignMany(targets, group, shouldHave));
|
|
6968
6982
|
ipcMain.handle('paste-many', (_, ids) => pasteMany(ids));
|
|
6983
|
+
ipcMain.handle('copy-many', (_, ids) => copyMany(ids));
|
|
6969
6984
|
ipcMain.handle('start-unify', (_, ids) => startUnify(ids));
|
|
6970
6985
|
ipcMain.handle('unify-step', (_, sessionId, payload) => unifyStep(sessionId, payload));
|
|
6971
6986
|
|
package/package.json
CHANGED
package/preload.js
CHANGED
|
@@ -9,7 +9,7 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
9
9
|
return () => ipcRenderer.removeListener('history-changed', listener);
|
|
10
10
|
},
|
|
11
11
|
getSettings: () => ipcRenderer.invoke('get-settings'),
|
|
12
|
-
|
|
12
|
+
copyClip: (id) => ipcRenderer.invoke('copy-clip', id),
|
|
13
13
|
pasteAndHide: (id) => ipcRenderer.invoke('paste-and-hide', id),
|
|
14
14
|
numpadPasteAndHide: (slot) => ipcRenderer.invoke('numpad-paste-and-hide', slot),
|
|
15
15
|
hidePopup: () => ipcRenderer.invoke('hide-popup'),
|
|
@@ -22,6 +22,7 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
22
22
|
restoreItems: (snaps) => ipcRenderer.invoke('restore-items', snaps),
|
|
23
23
|
groupAssignMany: (targets, group, shouldHave) => ipcRenderer.invoke('group-assign-many', targets, group, shouldHave), // [{ id, rev }]
|
|
24
24
|
pasteMany: (ids) => ipcRenderer.invoke('paste-many', ids),
|
|
25
|
+
copyMany: (ids) => ipcRenderer.invoke('copy-many', ids),
|
|
25
26
|
startUnify: (ids) => ipcRenderer.invoke('start-unify', ids),
|
|
26
27
|
deleteAll: () => ipcRenderer.invoke('delete-all'),
|
|
27
28
|
pin: (id, rev) => ipcRenderer.invoke('pin', id, rev),
|
package/site/index.html
CHANGED
|
@@ -859,20 +859,24 @@
|
|
|
859
859
|
deleteClip: (id) => { clips = Core.deleteItem(clips, id); },
|
|
860
860
|
clearUnpinned: () => { clips = clips.filter((clip) => Core.isPinned(clip)); },
|
|
861
861
|
setGroupSharedAi: (group) => { if (demoSharedGroups.has(group)) demoSharedGroups.delete(group); else demoSharedGroups.add(group); },
|
|
862
|
-
|
|
862
|
+
// Copy without pasting (the controller says "Copied"). A page cannot paste
|
|
863
|
+
// into another app, so the demo's click (activateClip) copies too.
|
|
864
|
+
copyClip: async (id) => {
|
|
863
865
|
const clip = demoAdapter.itemById(id);
|
|
864
866
|
if (!clip) return;
|
|
865
867
|
if (clip.type === "image") { const ok = await writeImageClipboard(clip).catch(() => false); if (!ok) await writeBrowserClipboard(clip.text || ""); }
|
|
866
868
|
else await writeBrowserClipboard(clip.text || "");
|
|
869
|
+
clips = Core.touchItem(clips, id);
|
|
870
|
+
renderClips();
|
|
871
|
+
},
|
|
872
|
+
// Copy all: the app's Paste all text (text clips, one per line).
|
|
873
|
+
copyMany: async (ids) => {
|
|
874
|
+
const text = ids.map((id) => demoAdapter.itemById(id)).filter((clip) => clip && clip.type !== "image").map((clip) => clip.text || "").join("\n");
|
|
875
|
+
if (text) await writeBrowserClipboard(text);
|
|
867
876
|
},
|
|
868
877
|
activateClip: async (id) => {
|
|
869
|
-
|
|
870
|
-
if (!clip) return;
|
|
871
|
-
if (clip.type === "image") { const ok = await writeImageClipboard(clip).catch(() => false); if (!ok) await writeBrowserClipboard(clip.text || ""); }
|
|
872
|
-
else await writeBrowserClipboard(clip.text || "");
|
|
873
|
-
clips = Core.touchItem(clips, id);
|
|
878
|
+
await demoAdapter.copyClip(id);
|
|
874
879
|
showToast("Copied");
|
|
875
|
-
renderClips();
|
|
876
880
|
},
|
|
877
881
|
editClip: (id, _itemEl, options) => openDemoEditor(id, { ...Core.editorFindFor(state.query), ...(options || {}) }),
|
|
878
882
|
// The open editor's title field is authoritative for its clip (it rides
|
|
@@ -898,7 +898,7 @@
|
|
|
898
898
|
// (popup, editor, viewer), so an action looks the same wherever it appears
|
|
899
899
|
// and no two actions share a glyph.
|
|
900
900
|
const CLIP_ACTION_ICONS = {
|
|
901
|
-
pin: 'star', edit: 'edit_note', 'open-img': 'open_in_full', 'open-img-ext': 'open_in_new', 'save-img': 'download',
|
|
901
|
+
copy: 'content_copy', pin: 'star', edit: 'edit_note', 'open-img': 'open_in_full', 'open-img-ext': 'open_in_new', 'save-img': 'download',
|
|
902
902
|
rename: 'drive_file_rename_outline', revert: 'history', 'select-similar': 'select_all', del: 'delete',
|
|
903
903
|
};
|
|
904
904
|
function renderClipActions(item, options) {
|
|
@@ -906,7 +906,9 @@
|
|
|
906
906
|
const id = itemId(item) || '';
|
|
907
907
|
const isImage = item && item.type === 'image';
|
|
908
908
|
const I = CLIP_ACTION_ICONS;
|
|
909
|
-
|
|
909
|
+
// Copy (no paste) leads every row: the clip goes on the clipboard and the
|
|
910
|
+
// popup stays.
|
|
911
|
+
let html = `<button class="icon-btn" data-action="copy" data-id="${escapeHtml(id)}" title="Copy" aria-label="Copy"><span class="mi">${I.copy}</span></button>`;
|
|
910
912
|
if (isImage) {
|
|
911
913
|
html += `<button class="icon-btn" data-action="open-img" data-id="${escapeHtml(id)}" title="Open image"><span class="mi">${I['open-img']}</span></button><button class="icon-btn" data-action="save-img" data-id="${escapeHtml(id)}" title="Copy to Downloads"><span class="mi">${I['save-img']}</span></button>`;
|
|
912
914
|
} else {
|
|
@@ -1025,6 +1027,7 @@
|
|
|
1025
1027
|
const nmap = opts.numpadMap || numpadMap(items);
|
|
1026
1028
|
const row = (action, label, cls, extra) => menuRowHtml({ icon: CLIP_ACTION_ICONS[action], label, cls, ...(extra || {}), attrs: { 'data-action': action, 'data-id': id, ...((extra && extra.attrs) || {}) } });
|
|
1027
1029
|
let html = '<div class="bc-menu-list">';
|
|
1030
|
+
html += row('copy', 'Copy');
|
|
1028
1031
|
html += row('pin', pinned ? 'Unpin' : 'Pin');
|
|
1029
1032
|
if (isImage) {
|
|
1030
1033
|
if (context !== 'viewer') html += row('open-img', 'Open image');
|
|
@@ -1071,6 +1074,7 @@
|
|
|
1071
1074
|
const row = (action, icon, label, cls) => menuRowHtml({ icon, label, cls, attrs: { 'data-action': action } });
|
|
1072
1075
|
let html = '<div class="bc-menu-list">';
|
|
1073
1076
|
html += row('bulk-paste', 'content_paste', `Paste all (${count})`);
|
|
1077
|
+
html += row('bulk-copy', CLIP_ACTION_ICONS.copy, `Copy all (${count})`);
|
|
1074
1078
|
html += submenuNodeHtml({ icon: 'sell', label: 'Add to group' }, bulkGroupTreeHtml(groups, selItems, Search.groupWeights(opts.items || [])));
|
|
1075
1079
|
if (allText) html += row('bulk-unify', 'merge', `Unify (${count})`);
|
|
1076
1080
|
html += MENU_SEPARATOR + row('bulk-delete', CLIP_ACTION_ICONS.del, `Delete (${count})`, 'danger');
|
|
@@ -1098,6 +1102,7 @@
|
|
|
1098
1102
|
return `<span class="selection-count">${count} selected</span>
|
|
1099
1103
|
<div class="selection-actions">
|
|
1100
1104
|
<button class="icon-btn" type="button" data-action="bulk-paste" title="Paste all (Enter)" aria-label="Paste all"><span class="mi">content_paste</span></button>
|
|
1105
|
+
<button class="icon-btn" type="button" data-action="bulk-copy" title="Copy all" aria-label="Copy all"><span class="mi">${CLIP_ACTION_ICONS.copy}</span></button>
|
|
1101
1106
|
<button class="icon-btn" type="button" data-action="bulk-group-open" title="Group" aria-label="Group" aria-haspopup="true"><span class="mi">sell</span></button>
|
|
1102
1107
|
${allText ? '<button class="icon-btn" type="button" data-action="bulk-unify" title="Unify into one clip" aria-label="Unify into one clip"><span class="mi">merge</span></button>' : ''}
|
|
1103
1108
|
<button class="icon-btn danger" type="button" data-action="bulk-delete" title="Delete selected" aria-label="Delete selected"><span class="mi">delete</span></button>
|
|
@@ -3438,8 +3443,9 @@
|
|
|
3438
3443
|
// mutate: pin(id), numpadAssign(id,slot), numpadUnassign(slot),
|
|
3439
3444
|
// toggleGroup(id,group) [add-or-remove], createGroup(name),
|
|
3440
3445
|
// deleteGroup(group), deleteClip(id), clearUnpinned(),
|
|
3441
|
-
// setGroupSharedAi(group)
|
|
3442
|
-
// actions: activateClip(id),
|
|
3446
|
+
// setGroupSharedAi(group)
|
|
3447
|
+
// actions: activateClip(id), copyClip(id) [the clipboard only, no paste],
|
|
3448
|
+
// copyMany(ids) [Paste all's joined text, no paste], editClip(id,itemEl), openImage(item),
|
|
3443
3449
|
// saveImage(item)->feedbackString|null
|
|
3444
3450
|
// keyboard:isSettingsOpen(), closeSettings(), hidePopup(),
|
|
3445
3451
|
// moveSelection(dir), activateSelected()
|
|
@@ -4500,6 +4506,14 @@
|
|
|
4500
4506
|
try { if (a.pasteMany) await a.pasteMany(ids); }
|
|
4501
4507
|
finally { clearSelection(); } // always drop the selection (app hides the popup; demo hides the bar)
|
|
4502
4508
|
}
|
|
4509
|
+
// Copy all: Paste all's joined text onto the clipboard, nothing pasted;
|
|
4510
|
+
// the popup and the selection stay.
|
|
4511
|
+
async function copySelection() {
|
|
4512
|
+
const ids = selectionInfo().ids;
|
|
4513
|
+
if (!ids.length || !a.copyMany) return;
|
|
4514
|
+
await a.copyMany(ids);
|
|
4515
|
+
toast('Copied');
|
|
4516
|
+
}
|
|
4503
4517
|
async function deleteIds(ids) {
|
|
4504
4518
|
if (!ids || !ids.length) return;
|
|
4505
4519
|
ids.forEach((id) => selectedIds.delete(id));
|
|
@@ -4709,6 +4723,16 @@
|
|
|
4709
4723
|
const bulkGroupOpen = t.closest('[data-action="bulk-group-open"]');
|
|
4710
4724
|
if (bulkGroupOpen) { event.stopPropagation(); const r = bulkGroupOpen.getBoundingClientRect(); openBulkGroupMenu(r.left, r.bottom + 2); return true; }
|
|
4711
4725
|
if (t.closest('[data-action="bulk-paste"]')) { event.stopPropagation(); pasteSelection(); return true; }
|
|
4726
|
+
if (t.closest('[data-action="bulk-copy"]')) { event.stopPropagation(); copySelection(); return true; }
|
|
4727
|
+
// Copy without pasting (a row's Copy, the clip menu's Copy): the row
|
|
4728
|
+
// button carries data-id, the menu root does (closest).
|
|
4729
|
+
const copyBtn = t.closest('[data-action="copy"]');
|
|
4730
|
+
if (copyBtn) {
|
|
4731
|
+
event.stopPropagation();
|
|
4732
|
+
const owner = copyBtn.dataset.id ? copyBtn : copyBtn.closest('[data-id]');
|
|
4733
|
+
if (owner && owner.dataset.id) { await a.copyClip(owner.dataset.id); toast('Copied'); }
|
|
4734
|
+
return true;
|
|
4735
|
+
}
|
|
4712
4736
|
if (t.closest('[data-action="bulk-unify"]')) { event.stopPropagation(); unifySelection(); return true; }
|
|
4713
4737
|
if (t.closest('[data-action="bulk-delete"]')) { event.stopPropagation(); deleteSelection(); return true; }
|
|
4714
4738
|
if (t.closest('[data-action="bulk-clear"]')) { event.stopPropagation(); clearSelection(); return true; }
|
|
@@ -4763,7 +4787,7 @@
|
|
|
4763
4787
|
const npRemove = t.closest('.np-remove');
|
|
4764
4788
|
if (npRemove) { event.stopPropagation(); await a.numpadUnassign(Number(npRemove.dataset.slot)); refresh(); return true; }
|
|
4765
4789
|
const slotEl = t.closest('.np-slot.has-content');
|
|
4766
|
-
if (slotEl && slotEl.dataset.slotId) { event.stopPropagation(); await a.
|
|
4790
|
+
if (slotEl && slotEl.dataset.slotId) { event.stopPropagation(); await a.copyClip(slotEl.dataset.slotId); toast('Copied'); return true; }
|
|
4767
4791
|
const share = t.closest('.gp-share');
|
|
4768
4792
|
if (share) { event.stopPropagation(); await a.setGroupSharedAi(share.dataset.group); refresh(); return true; }
|
|
4769
4793
|
const gpDel = t.closest('.gp-del');
|
|
@@ -4996,6 +5020,9 @@
|
|
|
4996
5020
|
// re-painting selection classes after a full list rebuild) + exposed for tests.
|
|
4997
5021
|
selection: selectionInfo,
|
|
4998
5022
|
focusedId: () => focusId,
|
|
5023
|
+
// An open menu or popover owns the keys (the app's empty-search digit
|
|
5024
|
+
// quick-paste must not fire under it).
|
|
5025
|
+
isMenuOpen: () => menu.isOpen(),
|
|
4999
5026
|
clearSelection,
|
|
5000
5027
|
selectAll,
|
|
5001
5028
|
moveFocus,
|