joplin-plugin-explorer 1.1.4 → 1.2.1

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.
@@ -1,485 +1 @@
1
- /* Webview script for Notes In List panel */
2
-
3
- // Persist scroll position across setHtml calls
4
- var _savedScrollTop = 0;
5
- var _isFirstRender = true;
6
-
7
- function postMsg(msg) {
8
- webviewApi.postMessage(msg);
9
- }
10
-
11
- // Save scroll position continuously
12
- document.addEventListener('scroll', function(e) {
13
- if (e.target && e.target.id === 'tree-container') {
14
- _savedScrollTop = e.target.scrollTop;
15
- }
16
- }, true);
17
-
18
- // Observe DOM changes to restore scroll position after setHtml
19
- var _observer = new MutationObserver(function() {
20
- var container = document.getElementById('tree-container');
21
- if (!container) return;
22
-
23
- if (_isFirstRender) {
24
- _isFirstRender = false;
25
- var selected = container.querySelector('.tree-item.note.selected');
26
- if (selected) {
27
- setTimeout(function() {
28
- selected.scrollIntoView({ block: 'nearest', behavior: 'instant' });
29
- }, 30);
30
- }
31
- } else {
32
- container.scrollTop = _savedScrollTop;
33
- }
34
- });
35
- _observer.observe(document.body, { childList: true, subtree: true });
36
-
37
- function loadI18n() {
38
- var root = document.getElementById('notes-in-list-root');
39
- if (root && root.dataset.i18n) {
40
- try { window._i18n = JSON.parse(root.dataset.i18n); } catch(e) {}
41
- }
42
- }
43
-
44
- function T(key) {
45
- if (!window._i18n) loadI18n();
46
- return (window._i18n && window._i18n[key]) || key;
47
- }
48
-
49
- // Inline input dialog (replaces prompt() which may not work in webview)
50
- function showInlineInput(label, defaultValue, callback) {
51
- var existing = document.getElementById('inline-input-overlay');
52
- if (existing) existing.remove();
53
-
54
- var overlay = document.createElement('div');
55
- overlay.id = 'inline-input-overlay';
56
- overlay.className = 'inline-input-overlay';
57
- overlay.innerHTML = '<div class="inline-input-dialog">'
58
- + '<div class="inline-input-label">' + label + '</div>'
59
- + '<input class="inline-input-field" type="text" value="' + (defaultValue || '').replace(/"/g, '&quot;') + '" />'
60
- + '<div class="inline-input-buttons">'
61
- + '<button class="inline-input-ok">OK</button>'
62
- + '<button class="inline-input-cancel">' + (T('cancel') || 'Cancel') + '</button>'
63
- + '</div></div>';
64
-
65
- document.body.appendChild(overlay);
66
-
67
- var input = overlay.querySelector('.inline-input-field');
68
- input.focus();
69
- input.select();
70
-
71
- function submit() {
72
- var val = input.value;
73
- overlay.remove();
74
- callback(val);
75
- }
76
- function cancel() {
77
- overlay.remove();
78
- callback(null);
79
- }
80
-
81
- overlay.querySelector('.inline-input-ok').addEventListener('click', submit);
82
- overlay.querySelector('.inline-input-cancel').addEventListener('click', cancel);
83
- input.addEventListener('keydown', function(e) {
84
- if (e.key === 'Enter') submit();
85
- if (e.key === 'Escape') cancel();
86
- });
87
- overlay.addEventListener('click', function(e) {
88
- if (e.target === overlay) cancel();
89
- });
90
- }
91
-
92
- // Left click: open note / toggle folder
93
- document.addEventListener('click', function(e) {
94
- var existingMenu = document.getElementById('ctx-menu');
95
- if (existingMenu) existingMenu.remove();
96
-
97
- var item = e.target.closest('.tree-item');
98
- if (!item) return;
99
-
100
- var type = item.dataset.type;
101
- var id = item.dataset.id;
102
-
103
- if (type === 'note') {
104
- document.querySelectorAll('.tree-item.note.selected').forEach(function(el) {
105
- el.classList.remove('selected');
106
- });
107
- item.classList.add('selected');
108
- postMsg({ name: 'openNote', id: id });
109
- }
110
-
111
- if (type === 'folder') {
112
- postMsg({ name: 'toggleFolder', id: id });
113
- }
114
- });
115
-
116
- // Right click: context menu
117
- document.addEventListener('contextmenu', function(e) {
118
- var existingMenu = document.getElementById('ctx-menu');
119
- if (existingMenu) existingMenu.remove();
120
-
121
- var item = e.target.closest('.tree-item');
122
- if (!item) return;
123
-
124
- e.preventDefault();
125
-
126
- var type = item.dataset.type;
127
- var id = item.dataset.id;
128
- var title = '';
129
- var labelEl = item.querySelector('.label');
130
- if (labelEl) title = labelEl.textContent;
131
-
132
- var menuHtml = '<div id="ctx-menu" class="context-menu" style="left:' + e.pageX + 'px;top:' + e.pageY + 'px;">';
133
-
134
- if (type === 'folder') {
135
- menuHtml += '<div class="ctx-item" data-action="newNote" data-id="' + id + '" data-type="folder">' + T('ctxNewNoteHere') + '</div>';
136
- menuHtml += '<div class="ctx-item" data-action="newTodo" data-id="' + id + '" data-type="folder">' + T('ctxNewTodoHere') + '</div>';
137
- menuHtml += '<div class="ctx-item" data-action="newSubNotebook" data-id="' + id + '" data-type="folder">' + T('ctxNewSubNotebook') + '</div>';
138
- menuHtml += '<div class="ctx-sep"></div>';
139
- menuHtml += '<div class="ctx-item" data-action="renameFolder" data-id="' + id + '" data-type="folder" data-title="' + title.replace(/"/g, '&quot;') + '">' + T('ctxRenameFolder') + '</div>';
140
- menuHtml += '<div class="ctx-item" data-action="exportFolder" data-id="' + id + '" data-type="folder">' + T('ctxExportFolder') + '</div>';
141
- menuHtml += '<div class="ctx-sep"></div>';
142
- menuHtml += '<div class="ctx-item ctx-danger" data-action="deleteFolder" data-id="' + id + '" data-type="folder">' + T('ctxDeleteFolder') + '</div>';
143
- } else if (type === 'note') {
144
- menuHtml += '<div class="ctx-item" data-action="openNote" data-id="' + id + '" data-type="note">' + T('ctxOpenNote') + '</div>';
145
- menuHtml += '<div class="ctx-item" data-action="openInNewWindow" data-id="' + id + '" data-type="note">' + T('ctxOpenInNewWindow') + '</div>';
146
- menuHtml += '<div class="ctx-sep"></div>';
147
- menuHtml += '<div class="ctx-item" data-action="copyLink" data-id="' + id + '" data-type="note">' + T('ctxCopyLink') + '</div>';
148
- menuHtml += '<div class="ctx-item" data-action="duplicateNote" data-id="' + id + '" data-type="note">' + T('ctxDuplicateNote') + '</div>';
149
- menuHtml += '<div class="ctx-item" data-action="switchNoteType" data-id="' + id + '" data-type="note">' + T('ctxSwitchNoteType') + '</div>';
150
- menuHtml += '<div class="ctx-item" data-action="toggleTodo" data-id="' + id + '" data-type="note">' + T('ctxToggleTodo') + '</div>';
151
- menuHtml += '<div class="ctx-sep"></div>';
152
- menuHtml += '<div class="ctx-item" data-action="renameNote" data-id="' + id + '" data-type="note" data-title="' + title.replace(/"/g, '&quot;') + '">' + T('ctxRenameNote') + '</div>';
153
- menuHtml += '<div class="ctx-item" data-action="noteInfo" data-id="' + id + '" data-type="note">' + T('ctxNoteInfo') + '</div>';
154
- menuHtml += '<div class="ctx-sep"></div>';
155
- menuHtml += '<div class="ctx-item ctx-danger" data-action="deleteNote" data-id="' + id + '" data-type="note">' + T('ctxDeleteNote') + '</div>';
156
- }
157
-
158
- menuHtml += '</div>';
159
- document.body.insertAdjacentHTML('beforeend', menuHtml);
160
-
161
- var menu = document.getElementById('ctx-menu');
162
- var rect = menu.getBoundingClientRect();
163
- if (rect.right > window.innerWidth) menu.style.left = (window.innerWidth - rect.width - 4) + 'px';
164
- if (rect.bottom > window.innerHeight) menu.style.top = (window.innerHeight - rect.height - 4) + 'px';
165
- });
166
-
167
- // Context menu item click
168
- document.addEventListener('click', function(e) {
169
- var ctxItem = e.target.closest('.ctx-item');
170
- if (!ctxItem) return;
171
-
172
- var action = ctxItem.dataset.action;
173
- var id = ctxItem.dataset.id;
174
- var itemType = ctxItem.dataset.type;
175
-
176
- // All dialogs handled by backend using native Joplin dialogs
177
- postMsg({ name: 'contextMenu', action: action, id: id, itemType: itemType });
178
-
179
- var menu = document.getElementById('ctx-menu');
180
- if (menu) menu.remove();
181
- });
182
-
183
- // Close context menu
184
- document.addEventListener('mousedown', function(e) {
185
- if (!e.target.closest('#ctx-menu')) {
186
- var menu = document.getElementById('ctx-menu');
187
- if (menu) menu.remove();
188
- }
189
- });
190
-
191
- // Toolbar buttons
192
- document.addEventListener('click', function(e) {
193
- var btn = e.target.closest('button');
194
- if (!btn) return;
195
-
196
- switch (btn.id) {
197
- case 'btn-new-notebook': postMsg({ name: 'newNotebook' }); break;
198
- case 'btn-new-note': postMsg({ name: 'newNote' }); break;
199
- case 'btn-new-todo': postMsg({ name: 'newTodo' }); break;
200
- case 'btn-sort': postMsg({ name: 'cycleSort' }); break;
201
- case 'btn-collapse-all': postMsg({ name: 'collapseAll' }); break;
202
- case 'btn-sync':
203
- var syncBtn = document.getElementById('btn-sync');
204
- if (syncBtn && !syncBtn.disabled) {
205
- syncBtn.disabled = true;
206
- syncBtn.classList.add('syncing');
207
- syncBtn.textContent = '\uD83D\uDD04 ' + T('syncing');
208
- postMsg({ name: 'sync' });
209
- }
210
- break;
211
- }
212
- });
213
-
214
- // Listen for messages from plugin backend
215
- webviewApi.onMessage(function(msg) {
216
- if (!msg || !msg.message) return;
217
- var m = msg.message;
218
-
219
- if (m.name === 'syncState') {
220
- var syncBtn = document.getElementById('btn-sync');
221
- if (!syncBtn) return;
222
- if (m.state === 'done') {
223
- syncBtn.classList.remove('syncing');
224
- syncBtn.classList.add('sync-done');
225
- syncBtn.textContent = T('syncDone');
226
- // Show "done" for 2 seconds, then restore
227
- setTimeout(function() {
228
- var btn = document.getElementById('btn-sync');
229
- if (btn) {
230
- btn.disabled = false;
231
- btn.classList.remove('sync-done');
232
- btn.textContent = '\uD83D\uDD04 ' + T('sync');
233
- }
234
- }, 2000);
235
- }
236
- } else if (m.name === 'searchResults') {
237
- // Ignore stale search results
238
- if (m.searchId !== undefined && m.searchId !== _searchId) return;
239
- if (m.results === null) {
240
- if (_searchMode) exitSearchMode();
241
- } else {
242
- renderSearchResults(m.results, m.query);
243
- }
244
- } else if (m.name === 'copyText') {
245
- // Fallback clipboard copy via webview
246
- if (navigator.clipboard && navigator.clipboard.writeText) {
247
- navigator.clipboard.writeText(m.text);
248
- } else {
249
- var ta = document.createElement('textarea');
250
- ta.value = m.text;
251
- document.body.appendChild(ta);
252
- ta.select();
253
- document.execCommand('copy');
254
- ta.remove();
255
- }
256
- } else if (m.name === 'selectNote') {
257
- // Update selection without full re-render
258
- document.querySelectorAll('.tree-item.note.selected').forEach(function(el) {
259
- el.classList.remove('selected');
260
- });
261
- var noteEl = document.querySelector('.tree-item.note[data-id="' + m.id + '"]');
262
- if (noteEl) {
263
- noteEl.classList.add('selected');
264
- noteEl.scrollIntoView({ block: 'nearest', behavior: 'instant' });
265
- }
266
- }
267
- });
268
-
269
- // ======================== Drag & Drop ========================
270
- // Make tree items draggable
271
- document.addEventListener('mousedown', function(e) {
272
- var item = e.target.closest('.tree-item');
273
- if (!item || e.button !== 0) return;
274
- item.setAttribute('draggable', 'true');
275
- });
276
-
277
- document.addEventListener('dragstart', function(e) {
278
- var item = e.target.closest('.tree-item');
279
- if (!item) return;
280
- e.dataTransfer.setData('text/plain', JSON.stringify({
281
- id: item.dataset.id,
282
- type: item.dataset.type,
283
- }));
284
- e.dataTransfer.effectAllowed = 'move';
285
- item.classList.add('dragging');
286
- });
287
-
288
- document.addEventListener('dragend', function(e) {
289
- var item = e.target.closest('.tree-item');
290
- if (item) {
291
- item.classList.remove('dragging');
292
- item.removeAttribute('draggable');
293
- }
294
- // Clean up all drop indicators
295
- document.querySelectorAll('.drop-target').forEach(function(el) { el.classList.remove('drop-target'); });
296
- document.querySelectorAll('.drop-above').forEach(function(el) { el.classList.remove('drop-above'); });
297
- document.querySelectorAll('.drop-below').forEach(function(el) { el.classList.remove('drop-below'); });
298
- });
299
-
300
- document.addEventListener('dragover', function(e) {
301
- var target = e.target.closest('.tree-item');
302
- if (!target) return;
303
-
304
- e.preventDefault();
305
- e.dataTransfer.dropEffect = 'move';
306
-
307
- // Clean previous indicators
308
- document.querySelectorAll('.drop-target').forEach(function(el) { el.classList.remove('drop-target'); });
309
- document.querySelectorAll('.drop-above').forEach(function(el) { el.classList.remove('drop-above'); });
310
- document.querySelectorAll('.drop-below').forEach(function(el) { el.classList.remove('drop-below'); });
311
-
312
- var targetType = target.dataset.type;
313
- var rect = target.getBoundingClientRect();
314
- var y = e.clientY - rect.top;
315
- var height = rect.height;
316
-
317
- if (targetType === 'folder') {
318
- // Top 25%: drop above, middle 50%: drop into, bottom 25%: drop below
319
- if (y < height * 0.25) {
320
- target.classList.add('drop-above');
321
- } else if (y > height * 0.75) {
322
- target.classList.add('drop-below');
323
- } else {
324
- target.classList.add('drop-target');
325
- }
326
- } else {
327
- // Notes: just show drop-target (will move to same folder)
328
- if (y < height * 0.5) {
329
- target.classList.add('drop-above');
330
- } else {
331
- target.classList.add('drop-below');
332
- }
333
- }
334
- });
335
-
336
- document.addEventListener('dragleave', function(e) {
337
- var target = e.target.closest('.tree-item');
338
- if (target) {
339
- target.classList.remove('drop-target');
340
- target.classList.remove('drop-above');
341
- target.classList.remove('drop-below');
342
- }
343
- });
344
-
345
- document.addEventListener('drop', function(e) {
346
- e.preventDefault();
347
- var target = e.target.closest('.tree-item');
348
- if (!target) return;
349
-
350
- var data;
351
- try { data = JSON.parse(e.dataTransfer.getData('text/plain')); } catch(err) { return; }
352
-
353
- var targetId = target.dataset.id;
354
- var targetType = target.dataset.type;
355
- var dragId = data.id;
356
- var dragType = data.type;
357
-
358
- if (dragId === targetId) return; // Can't drop on self
359
-
360
- var rect = target.getBoundingClientRect();
361
- var y = e.clientY - rect.top;
362
- var height = rect.height;
363
-
364
- if (targetType === 'folder') {
365
- if (y >= height * 0.25 && y <= height * 0.75) {
366
- // Drop INTO folder
367
- postMsg({ name: 'dragDrop', dragId: dragId, dragType: dragType, targetId: targetId, position: 'into' });
368
- } else {
369
- // Drop above/below folder (reorder)
370
- var pos = y < height * 0.25 ? 'above' : 'below';
371
- postMsg({ name: 'dragDrop', dragId: dragId, dragType: dragType, targetId: targetId, position: pos });
372
- }
373
- } else {
374
- // Drop on note -> move to same folder as note
375
- postMsg({ name: 'dragDrop', dragId: dragId, dragType: dragType, targetId: targetId, position: 'into' });
376
- }
377
-
378
- // Clean up
379
- document.querySelectorAll('.drop-target, .drop-above, .drop-below').forEach(function(el) {
380
- el.classList.remove('drop-target', 'drop-above', 'drop-below');
381
- });
382
- });
383
-
384
- // ======================== Content Search ========================
385
- var _searchTimer = null;
386
- var _searchMode = false;
387
- var _searchId = 0;
388
-
389
- function escapeRegex(str) {
390
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
391
- }
392
-
393
- function highlightText(text, query) {
394
- if (!query) return text;
395
- // Escape HTML first
396
- var escaped = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
397
- var regex = new RegExp('(' + escapeRegex(query) + ')', 'gi');
398
- return escaped.replace(regex, '<mark class="search-highlight">$1</mark>');
399
- }
400
-
401
- function showSearchContainer(show) {
402
- var tree = document.getElementById('tree-container');
403
- var search = document.getElementById('search-results');
404
- if (tree) tree.style.display = show ? 'none' : '';
405
- if (search) search.style.display = show ? '' : 'none';
406
- }
407
-
408
- function renderSearchResults(results, query) {
409
- var container = document.getElementById('search-results');
410
- if (!container) return;
411
-
412
- showSearchContainer(true);
413
-
414
- if (!results || results.length === 0) {
415
- container.innerHTML = '<div class="search-status">' + T('searchNoResult') + '</div>';
416
- return;
417
- }
418
-
419
- var countText = T('searchResultCount').replace('{count}', results.length);
420
- var html = '<div class="search-status">' + countText + '</div>';
421
-
422
- for (var i = 0; i < results.length; i++) {
423
- var item = results[i];
424
- var icon = '\uD83D\uDCDD';
425
- if (item.is_todo) {
426
- icon = item.todo_completed ? '\u2611' : '\u2610';
427
- }
428
- html += '<div class="search-result-item tree-item note" data-id="' + item.id + '" data-type="note">';
429
- html += '<span class="icon note-icon">' + icon + '</span>';
430
- html += '<div class="search-result-content">';
431
- html += '<div class="search-result-title">' + highlightText(item.title, query) + '</div>';
432
- if (item.folderName) {
433
- html += '<div class="search-result-folder">\uD83D\uDCC2 ' + item.folderName + '</div>';
434
- }
435
- if (item.snippet) {
436
- html += '<div class="search-result-snippet">' + highlightText(item.snippet, query) + '</div>';
437
- }
438
- html += '</div></div>';
439
- }
440
-
441
- container.innerHTML = html;
442
- _searchMode = true;
443
- }
444
-
445
- function exitSearchMode() {
446
- _searchMode = false;
447
- showSearchContainer(false);
448
- }
449
-
450
- document.addEventListener('input', function(e) {
451
- if (e.target.id !== 'search-input') return;
452
- var query = e.target.value.trim();
453
-
454
- if (_searchTimer) clearTimeout(_searchTimer);
455
- _searchId++;
456
- var currentSearchId = _searchId;
457
-
458
- if (!query) {
459
- if (_searchMode) exitSearchMode();
460
- return;
461
- }
462
-
463
- _searchMode = true;
464
- var searchContainer = document.getElementById('search-results');
465
- if (searchContainer) {
466
- searchContainer.innerHTML = '<div class="search-status">' + T('searching') + '</div>';
467
- }
468
- showSearchContainer(true);
469
-
470
- // Debounce: wait 400ms after typing stops
471
- _searchTimer = setTimeout(function() {
472
- postMsg({ name: 'search', query: query, searchId: currentSearchId });
473
- }, 400);
474
- });
475
-
476
- // Handle Escape key to clear search
477
- document.addEventListener('keydown', function(e) {
478
- if (e.key === 'Escape') {
479
- var input = document.getElementById('search-input');
480
- if (input && input.value) {
481
- input.value = '';
482
- if (_searchMode) exitSearchMode();
483
- }
484
- }
485
- });
1
+ var _savedScrollTop=0,_isFirstRender=!0;function postMsg(e){webviewApi.postMessage(e)}document.addEventListener("scroll",function(e){e.target&&"tree-container"===e.target.id&&(_savedScrollTop=e.target.scrollTop)},!0);var _observer=new MutationObserver(function(){var e=document.getElementById("tree-container");if(e)if(_isFirstRender){_isFirstRender=!1;var t=e.querySelector(".tree-item.note.selected");t&&setTimeout(function(){t.scrollIntoView({block:"nearest",behavior:"instant"})},30)}else e.scrollTop=_savedScrollTop});function loadI18n(){var e=document.getElementById("notes-in-list-root");if(e&&e.dataset.i18n)try{window._i18n=JSON.parse(e.dataset.i18n)}catch(e){}}function T(e){return window._i18n||loadI18n(),window._i18n&&window._i18n[e]||e}function getPinnedData(){var e=document.getElementById("notes-in-list-root");if(e&&e.dataset.pinned)try{return JSON.parse(e.dataset.pinned)}catch(e){}return[]}function isPinned(e){for(var t=getPinnedData(),a=0;a<t.length;a++)if(t[a].id===e)return!0;return!1}function showInlineInput(e,t,a){var n=document.getElementById("inline-input-overlay");n&&n.remove();var i=document.createElement("div");i.id="inline-input-overlay",i.className="inline-input-overlay",i.innerHTML='<div class="inline-input-dialog"><div class="inline-input-label">'+e+'</div><input class="inline-input-field" type="text" value="'+(t||"").replace(/"/g,"&quot;")+'" /><div class="inline-input-buttons"><button class="inline-input-ok">OK</button><button class="inline-input-cancel">'+(T("cancel")||"Cancel")+"</button></div></div>",document.body.appendChild(i);var s=i.querySelector(".inline-input-field");function d(){var e=s.value;i.remove(),a(e)}function o(){i.remove(),a(null)}s.focus(),s.select(),i.querySelector(".inline-input-ok").addEventListener("click",d),i.querySelector(".inline-input-cancel").addEventListener("click",o),s.addEventListener("keydown",function(e){"Enter"===e.key&&d(),"Escape"===e.key&&o()}),i.addEventListener("click",function(e){e.target===i&&o()})}function clearDropIndicators(){document.querySelectorAll(".drop-target").forEach(function(e){e.classList.remove("drop-target")}),document.querySelectorAll(".drop-above").forEach(function(e){e.classList.remove("drop-above")}),document.querySelectorAll(".drop-below").forEach(function(e){e.classList.remove("drop-below")})}function endDrag(){clearDropIndicators();var e=document.getElementById("tree-container");e&&e.classList.remove("dragging-active")}_observer.observe(document.body,{childList:!0,subtree:!0}),document.addEventListener("click",function(e){var t=document.getElementById("ctx-menu");t&&t.remove();var a=e.target.closest(".search-tag-item");if(a){var n=a.dataset.tagId;n&&postMsg({name:"loadTagNotes",tagId:n})}else{var i=e.target.closest(".search-folder-item");if(i){var s=i.dataset.folderId;s&&postMsg({name:"locateFolder",folderId:s})}else{var d=e.target.closest(".tree-item");if(d){var o=d.dataset.type,r=d.dataset.id,c=d.classList.contains("pinned-item");"note"===o&&(document.querySelectorAll(".tree-item.note.selected").forEach(function(e){e.classList.remove("selected")}),d.classList.add("selected"),postMsg({name:"openNote",id:r})),"folder"===o&&postMsg(c?{name:"locatePinnedFolder",folderId:r}:{name:"toggleFolder",id:r})}}}}),document.addEventListener("contextmenu",function(e){var t=document.getElementById("ctx-menu");t&&t.remove();var a=e.target.closest(".tree-item");if(a){e.preventDefault();var n=a.dataset.type,i=a.dataset.id,s="",d=a.querySelector(".label");d&&(s=d.textContent);var o='<div id="ctx-menu" class="context-menu" style="left:'+e.pageX+"px;top:"+e.pageY+'px;">';if("folder"===n){var r=isPinned(i);o+='<div class="ctx-item" data-action="'+(r?"unpinFolder":"pinFolder")+'" data-id="'+i+'" data-type="folder">'+T(r?"ctxUnpin":"ctxPin")+"</div>",o+='<div class="ctx-sep"></div>',o+='<div class="ctx-item" data-action="newNote" data-id="'+i+'" data-type="folder">'+T("ctxNewNoteHere")+"</div>",o+='<div class="ctx-item" data-action="newTodo" data-id="'+i+'" data-type="folder">'+T("ctxNewTodoHere")+"</div>",o+='<div class="ctx-item" data-action="newSubNotebook" data-id="'+i+'" data-type="folder">'+T("ctxNewSubNotebook")+"</div>",o+='<div class="ctx-sep"></div>',o+='<div class="ctx-item" data-action="renameFolder" data-id="'+i+'" data-type="folder" data-title="'+s.replace(/"/g,"&quot;")+'">'+T("ctxRenameFolder")+"</div>",o+='<div class="ctx-item" data-action="exportFolder" data-id="'+i+'" data-type="folder">'+T("ctxExportFolder")+"</div>",o+='<div class="ctx-sep"></div>',o+='<div class="ctx-item ctx-danger" data-action="deleteFolder" data-id="'+i+'" data-type="folder">'+T("ctxDeleteFolder")+"</div>"}else if("note"===n){var c=isPinned(i);o+='<div class="ctx-item" data-action="'+(c?"unpinNote":"pinNote")+'" data-id="'+i+'" data-type="note">'+T(c?"ctxUnpin":"ctxPin")+"</div>",o+='<div class="ctx-sep"></div>',o+='<div class="ctx-item" data-action="openNote" data-id="'+i+'" data-type="note">'+T("ctxOpenNote")+"</div>",o+='<div class="ctx-item" data-action="openInNewWindow" data-id="'+i+'" data-type="note">'+T("ctxOpenInNewWindow")+"</div>",o+='<div class="ctx-sep"></div>',o+='<div class="ctx-item" data-action="copyLink" data-id="'+i+'" data-type="note">'+T("ctxCopyLink")+"</div>",o+='<div class="ctx-item" data-action="duplicateNote" data-id="'+i+'" data-type="note">'+T("ctxDuplicateNote")+"</div>",o+='<div class="ctx-item" data-action="switchNoteType" data-id="'+i+'" data-type="note">'+T("ctxSwitchNoteType")+"</div>",o+='<div class="ctx-item" data-action="toggleTodo" data-id="'+i+'" data-type="note">'+T("ctxToggleTodo")+"</div>",o+='<div class="ctx-sep"></div>',o+='<div class="ctx-item" data-action="renameNote" data-id="'+i+'" data-type="note" data-title="'+s.replace(/"/g,"&quot;")+'">'+T("ctxRenameNote")+"</div>",o+='<div class="ctx-item" data-action="noteInfo" data-id="'+i+'" data-type="note">'+T("ctxNoteInfo")+"</div>",o+='<div class="ctx-sep"></div>',o+='<div class="ctx-item ctx-danger" data-action="deleteNote" data-id="'+i+'" data-type="note">'+T("ctxDeleteNote")+"</div>"}o+="</div>",document.body.insertAdjacentHTML("beforeend",o);var l=document.getElementById("ctx-menu"),v=l.getBoundingClientRect();v.right>window.innerWidth&&(l.style.left=window.innerWidth-v.width-4+"px"),v.bottom>window.innerHeight&&(l.style.top=window.innerHeight-v.height-4+"px")}}),document.addEventListener("click",function(e){var t=e.target.closest(".ctx-item");if(t){postMsg({name:"contextMenu",action:t.dataset.action,id:t.dataset.id,itemType:t.dataset.type});var a=document.getElementById("ctx-menu");a&&a.remove()}}),document.addEventListener("mousedown",function(e){if(!e.target.closest("#ctx-menu")){var t=document.getElementById("ctx-menu");t&&t.remove()}}),document.addEventListener("click",function(e){e.target.closest(".pinned-section-header")&&postMsg({name:"togglePinnedCollapse"})}),document.addEventListener("click",function(e){var t=e.target.closest(".search-section-header");if(t){var a=t.dataset.section,n=document.getElementById("search-section-"+a);if(n){var i=t.querySelector(".section-toggle");n.classList.contains("collapsed")?(n.classList.remove("collapsed"),i&&(i.textContent="▼")):(n.classList.add("collapsed"),i&&(i.textContent="▶"))}}}),document.addEventListener("click",function(e){var t=e.target.closest("button");if(t)switch(t.id){case"btn-new-notebook":postMsg({name:"newNotebook"});break;case"btn-new-note":postMsg({name:"newNote"});break;case"btn-new-todo":postMsg({name:"newTodo"});break;case"btn-sort":postMsg({name:"cycleSort"});break;case"btn-collapse-all":postMsg({name:"collapseAll"});break;case"btn-sync":var a=document.getElementById("btn-sync");a&&!a.disabled&&(a.disabled=!0,a.classList.add("syncing"),a.textContent="🔄 "+T("syncing"),postMsg({name:"sync"}))}}),webviewApi.onMessage(function(e){if(e&&e.message){var t=e.message;if("syncState"===t.name){var a=document.getElementById("btn-sync");if(!a)return;"done"===t.state&&(a.classList.remove("syncing"),a.classList.add("sync-done"),a.textContent=T("syncDone"),setTimeout(function(){var e=document.getElementById("btn-sync");e&&(e.disabled=!1,e.classList.remove("sync-done"),e.textContent="🔄 "+T("sync"))},2e3))}else if("searchResults"===t.name){if(void 0!==t.searchId&&t.searchId!==_searchId)return;t.notes||t.tags||t.folders?renderSearchResults(t.notes||[],t.tags||[],t.folders||[],t.query):_searchMode&&exitSearchMode()}else if("tagNotes"===t.name)expandTagNotes(t.tagId,t.notes);else if("scrollToFolder"===t.name)setTimeout(function(){var e=document.querySelector('.tree-item.folder:not(.pinned-item)[data-id="'+t.folderId+'"]');e&&(e.scrollIntoView({block:"center",behavior:"smooth"}),e.classList.add("locate-flash"),setTimeout(function(){e.classList.remove("locate-flash")},1500))},80);else if("exitSearch"===t.name){var n=document.getElementById("search-input");n&&(n.value=""),_searchMode&&exitSearchMode()}else if("exitSearchAndLocate"===t.name){var i=document.getElementById("search-input");i&&(i.value=""),_searchMode=!1,showSearchContainer(!1),setTimeout(function(){var e=document.querySelector('.tree-item.folder[data-id="'+t.folderId+'"]');e&&(e.scrollIntoView({block:"center",behavior:"smooth"}),e.classList.add("locate-flash"),setTimeout(function(){e.classList.remove("locate-flash")},1500))},80)}else if("copyText"===t.name)if(navigator.clipboard&&navigator.clipboard.writeText)navigator.clipboard.writeText(t.text);else{var s=document.createElement("textarea");s.value=t.text,document.body.appendChild(s),s.select(),document.execCommand("copy"),s.remove()}else if("selectNote"===t.name){document.querySelectorAll(".tree-item.note.selected").forEach(function(e){e.classList.remove("selected")});var d=document.querySelector('.tree-item.note[data-id="'+t.id+'"]');d&&(d.classList.add("selected"),d.scrollIntoView({block:"nearest",behavior:"instant"}))}}}),document.addEventListener("mousedown",function(e){var t=e.target.closest(".tree-item");t&&0===e.button&&t.setAttribute("draggable","true")}),document.addEventListener("dragstart",function(e){var t=e.target.closest(".tree-item");if(t){var a=t.classList.contains("pinned-item");e.dataTransfer.setData("text/plain",JSON.stringify({id:t.dataset.id,type:t.dataset.type,pinned:a})),e.dataTransfer.effectAllowed="move",t.classList.add("dragging");var n=document.getElementById("tree-container");n&&n.classList.add("dragging-active")}}),document.addEventListener("dragend",function(e){var t=e.target.closest(".tree-item");t&&(t.classList.remove("dragging"),t.removeAttribute("draggable")),endDrag()}),document.addEventListener("dragover",function(e){var t=e.target;if(t&&3===t.nodeType&&(t=t.parentElement),t){var a=t.closest(".tree-item");if(document.getElementById("tree-container"),t.closest("#drop-zone-empty"))return e.preventDefault(),void(e.dataTransfer.dropEffect="move");if(t.closest(".pinned-section-header")||t.closest(".pinned-section-body")){if(e.preventDefault(),e.dataTransfer.dropEffect="move",clearDropIndicators(),a&&a.classList.contains("pinned-item")){var n=a.getBoundingClientRect();e.clientY-n.top<.5*n.height?a.classList.add("drop-above"):a.classList.add("drop-below")}}else if(a){var i=document.querySelector(".tree-item.dragging");if(!i||!i.classList.contains("pinned-item")){e.preventDefault(),e.dataTransfer.dropEffect="move",clearDropIndicators();var s=a.dataset.type,d=a.getBoundingClientRect(),o=e.clientY-d.top,r=d.height;"folder"===s?o<.25*r?a.classList.add("drop-above"):o>.75*r?a.classList.add("drop-below"):a.classList.add("drop-target"):o<.5*r?a.classList.add("drop-above"):a.classList.add("drop-below")}}}}),document.addEventListener("dragleave",function(e){var t=e.target.closest(".tree-item");t&&(t.classList.remove("drop-target"),t.classList.remove("drop-above"),t.classList.remove("drop-below"));var a=document.getElementById("tree-container");a&&e.target===a&&a.classList.remove("drop-empty")}),document.addEventListener("drop",function(e){e.preventDefault();var t=e.target;t&&3===t.nodeType&&(t=t.parentElement);var a,n=t?t.closest(".tree-item"):null;document.getElementById("tree-container");try{a=JSON.parse(e.dataTransfer.getData("text/plain"))}catch(e){return void endDrag()}var i=a.id,s=a.type,d=a.pinned;if(t&&t.closest("#drop-zone-empty"))return postMsg({name:"dragToEmpty",dragId:i,dragType:s}),void endDrag();if(t&&(t.closest(".pinned-section-header")||t.closest(".pinned-section-body"))){if(d){var o=n&&n.classList.contains("pinned-item")?n:null;if(o&&o.dataset.id!==i){var r=o.getBoundingClientRect(),c=e.clientY-r.top<.5*r.height?"before":"after";postMsg({name:"reorderPinned",dragId:i,dragType:s,targetId:o.dataset.id,position:c})}}else"note"===s?postMsg({name:"contextMenu",action:"pinNote",id:i,itemType:"note"}):"folder"===s&&postMsg({name:"contextMenu",action:"pinFolder",id:i,itemType:"folder"});endDrag()}else if(d)endDrag();else if(n){var l=n.dataset.id,v=n.dataset.type;if(i!==l){var u=n.getBoundingClientRect(),m=e.clientY-u.top,p=u.height;postMsg("folder"===v?m>=.25*p&&m<=.75*p?{name:"dragDrop",dragId:i,dragType:s,targetId:l,position:"into"}:{name:"dragDrop",dragId:i,dragType:s,targetId:l,position:m<.25*p?"above":"below"}:{name:"dragDrop",dragId:i,dragType:s,targetId:l,position:"into"}),endDrag()}else endDrag()}else endDrag()});var _searchTimer=null,_searchMode=!1,_searchId=0;function escapeRegex(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function highlightText(e,t){if(!t)return e;var a=e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;"),n=new RegExp("("+escapeRegex(t)+")","gi");return a.replace(n,'<mark class="search-highlight">$1</mark>')}function showSearchContainer(e){var t=document.getElementById("tree-container"),a=document.getElementById("search-results");t&&(t.style.display=e?"none":""),a&&(a.style.display=e?"":"none")}function renderNoteItem(e,t){var a="📝";e.is_todo&&(a=e.todo_completed?"☑":"☐");var n='<div class="search-result-item tree-item note" data-id="'+e.id+'" data-type="note">';return n+='<span class="icon note-icon">'+a+"</span>",n+='<div class="search-result-content">',n+='<div class="search-result-title">'+highlightText(e.title,t)+"</div>",e.folderName&&(n+='<div class="search-result-folder">📂 '+e.folderName+"</div>"),e.snippet&&(n+='<div class="search-result-snippet">'+highlightText(e.snippet,t)+"</div>"),n+"</div></div>"}function renderSearchResults(e,t,a,n){var i=document.getElementById("search-results");if(i){showSearchContainer(!0);var s=e.length+t.length+a.length;if(0!==s){var d='<div class="search-status">'+T("searchResultCount").replace("{count}",s)+"</div>";if(a.length>0){d+=p("📁",T("searchSectionFolders"),a.length,"folders"),d+='<div class="search-section-body" id="search-section-folders">';for(var o=0;o<a.length;o++){var r=a[o],c=r.icon&&r.icon.emoji?r.icon.emoji:"📁";d+='<div class="search-result-item search-folder-item" data-folder-id="'+r.id+'">',d+='<span class="icon">'+c+"</span>",d+='<div class="search-result-content">',d+='<div class="search-result-title">'+highlightText(r.title,n)+"</div>",d+="</div></div>"}d+="</div>"}if(e.length>0){d+=p("📝",T("searchSectionNotes"),e.length,"notes"),d+='<div class="search-section-body" id="search-section-notes">';for(var l=0;l<e.length;l++)d+=renderNoteItem(e[l],n);d+="</div>"}if(t.length>0){d+=p("🏷️",T("searchSectionTags"),t.length,"tags"),d+='<div class="search-section-body" id="search-section-tags">';for(var v=0;v<t.length;v++){var u=t[v],m=T("searchTagNoteCount").replace("{count}",u.noteCount);d+='<div class="search-result-item search-tag-item" data-tag-id="'+u.id+'">',d+='<span class="icon">🏷️</span>',d+='<div class="search-result-content">',d+='<div class="search-result-title">'+highlightText(u.title,n)+"</div>",d+='<div class="search-result-folder">'+m+"</div>",d+="</div>",d+='<span class="tag-expand-arrow">▶</span>',d+="</div>",d+='<div class="tag-notes-container" id="tag-notes-'+u.id+'"></div>'}d+="</div>"}i.innerHTML=d,_searchMode=!0}else i.innerHTML='<div class="search-status">'+T("searchNoResult")+"</div>"}function p(e,t,a,n){return'<div class="search-section-header" data-section="'+n+'"><span class="section-toggle">▼</span> '+e+" "+t+" ("+a+")</div>"}}function expandTagNotes(e,t){var a=document.getElementById("tag-notes-"+e);if(a){var n;if(a.children.length>0)return a.innerHTML="",void((n=document.querySelector('.search-tag-item[data-tag-id="'+e+'"] .tag-expand-arrow'))&&n.classList.remove("expanded"));for(var i="",s=0;s<t.length;s++)i+=renderNoteItem(t[s],"");0===t.length&&(i='<div class="search-status" style="padding-left:30px;">'+T("searchNoResult")+"</div>"),a.innerHTML=i,(n=document.querySelector('.search-tag-item[data-tag-id="'+e+'"] .tag-expand-arrow'))&&n.classList.add("expanded")}}function exitSearchMode(){_searchMode=!1,showSearchContainer(!1)}document.addEventListener("input",function(e){if("search-input"===e.target.id){var t=e.target.value.trim();_searchTimer&&clearTimeout(_searchTimer);var a=++_searchId;if(t){_searchMode=!0;var n=document.getElementById("search-results");n&&(n.innerHTML='<div class="search-status">'+T("searching")+"</div>"),showSearchContainer(!0),_searchTimer=setTimeout(function(){postMsg({name:"search",query:t,searchId:a})},400)}else _searchMode&&exitSearchMode()}}),document.addEventListener("keydown",function(e){if("Escape"===e.key){var t=document.getElementById("search-input");t&&t.value&&(t.value="",_searchMode&&exitSearchMode())}});