domma-cms 0.9.1 → 0.9.6
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/admin/js/templates/block-editor.html +163 -163
- package/admin/js/templates/form-editor.html +245 -245
- package/admin/js/views/action-editor.js +1 -1
- package/admin/js/views/block-editor.js +8 -8
- package/admin/js/views/collection-editor.js +4 -4
- package/admin/js/views/collections.js +1 -1
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/forms.js +1 -1
- package/admin/js/views/navigation.js +14 -14
- package/admin/js/views/page-editor.js +35 -35
- package/admin/js/views/pages.js +5 -5
- package/admin/js/views/plugins.js +13 -10
- package/admin/js/views/view-editor.js +1 -1
- package/config/plugins.json +25 -0
- package/package.json +1 -1
- package/plugins/docs/data/documents/57e003f0-68f2-47dc-9c36-ed4b10ed3deb.json +4 -4
- package/plugins/docs/data/folders.json +3 -3
- package/plugins/docs/data/versions/57e003f0-68f2-47dc-9c36-ed4b10ed3deb/1.json +5 -0
- package/plugins/garage/admin/templates/garage.html +30 -0
- package/plugins/garage/admin/views/garage.js +62 -1
- package/plugins/garage/plugin.json +1 -1
- package/plugins/notes/admin/templates/notes.html +2 -11
- package/plugins/notes/admin/views/notes.js +108 -129
- package/plugins/notes/collections/user-notes/schema.json +2 -1
- package/plugins/notes/plugin.json +1 -1
- package/plugins/site-search/admin/templates/site-search.html +174 -46
- package/plugins/site-search/admin/views/site-search.js +72 -1
- package/plugins/site-search/config.js +6 -1
- package/plugins/site-search/plugin.json +1 -1
- package/plugins/site-search/public/inject-head.html +1 -1
- package/plugins/site-search/public/search.css +1 -1
- package/plugins/site-search/public/search.js +1 -1
- package/plugins/todo/admin/templates/todo.html +2 -8
- package/plugins/todo/admin/views/todo.js +123 -106
- package/plugins/todo/collections/todos/schema.json +2 -1
- package/plugins/todo/plugin.json +1 -1
- package/server/routes/api/media.js +127 -118
- package/server/routes/api/plugins.js +15 -4
- package/server/server.js +288 -285
- package/server/services/collections.js +17 -10
- package/server/services/plugins.js +77 -67
- package/server/services/renderer.js +3 -3
- package/plugins/docs/data/documents/452f49b7-9c93-4a67-874d-27f882891ad2.json +0 -11
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
[
|
|
2
2
|
{
|
|
3
|
-
"id": "
|
|
4
|
-
"name": "
|
|
3
|
+
"id": "d62d4e48-39c8-4602-9c46-f5235a6f169c",
|
|
4
|
+
"name": "Personal",
|
|
5
5
|
"parentId": null,
|
|
6
6
|
"userId": "2421ad8e-060d-4548-8878-af7011d5e08b",
|
|
7
|
-
"createdAt": "2026-
|
|
7
|
+
"createdAt": "2026-04-03T15:07:14.590Z"
|
|
8
8
|
}
|
|
9
9
|
]
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
<button class="tab-item active">Results</button>
|
|
34
34
|
<button class="tab-item" id="tab-btn-garage">My Garage</button>
|
|
35
35
|
<button class="tab-item" id="tab-btn-history">History</button>
|
|
36
|
+
<button class="tab-item" id="tab-btn-settings"><span data-icon="settings"></span> Settings</button>
|
|
36
37
|
</div>
|
|
37
38
|
<div class="tab-content">
|
|
38
39
|
|
|
@@ -75,6 +76,35 @@
|
|
|
75
76
|
</div>
|
|
76
77
|
</div>
|
|
77
78
|
|
|
79
|
+
<!-- Settings Tab -->
|
|
80
|
+
<div class="tab-panel" id="tab-settings">
|
|
81
|
+
<div style="max-width:480px;margin-top:1rem;">
|
|
82
|
+
<div class="card">
|
|
83
|
+
<div class="card-header" style="display:flex;align-items:center;gap:0.5rem;">
|
|
84
|
+
<span data-icon="key"></span>
|
|
85
|
+
<strong>DVLA API Configuration</strong>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="card-body">
|
|
88
|
+
<p style="font-size:0.87rem;color:var(--dm-text-muted,#888);margin:0 0 1rem;">
|
|
89
|
+
An API key from the DVLA Vehicle Enquiry Service is required to look up vehicle details.
|
|
90
|
+
Obtain yours at the
|
|
91
|
+
<a href="https://developer-portal.driver-vehicle-licensing.api.gov.uk" target="_blank"
|
|
92
|
+
rel="noopener">DVLA developer portal</a>.
|
|
93
|
+
</p>
|
|
94
|
+
<div id="settings-key-status" style="margin-bottom:1rem;font-size:0.87rem;"></div>
|
|
95
|
+
<label style="display:block;font-size:0.9rem;font-weight:500;margin-bottom:0.35rem;">API
|
|
96
|
+
Key</label>
|
|
97
|
+
<input id="settings-api-key" type="password" class="form-input"
|
|
98
|
+
placeholder="Paste your DVLA API key" autocomplete="off"
|
|
99
|
+
style="width:100%;margin-bottom:1rem;">
|
|
100
|
+
<button id="settings-save-btn" class="btn btn-primary">
|
|
101
|
+
<span data-icon="save"></span> Save
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
78
108
|
</div>
|
|
79
109
|
</div>
|
|
80
110
|
|
|
@@ -162,6 +162,9 @@ export const garageView = {
|
|
|
162
162
|
const clearHistoryBtn = $container.find('#clear-history-btn').get(0);
|
|
163
163
|
const tabBtnGarage = $container.find('#tab-btn-garage').get(0);
|
|
164
164
|
const tabBtnHistory = $container.find('#tab-btn-history').get(0);
|
|
165
|
+
const settingsApiKeyInput = $container.find('#settings-api-key').get(0);
|
|
166
|
+
const settingsSaveBtn = $container.find('#settings-save-btn').get(0);
|
|
167
|
+
const settingsKeyStatus = $container.find('#settings-key-status').get(0);
|
|
165
168
|
|
|
166
169
|
// ---------------------------------------------------------------
|
|
167
170
|
// Tabs
|
|
@@ -552,10 +555,68 @@ export const garageView = {
|
|
|
552
555
|
await loadHistory();
|
|
553
556
|
});
|
|
554
557
|
|
|
558
|
+
// ---------------------------------------------------------------
|
|
559
|
+
// Settings
|
|
560
|
+
// ---------------------------------------------------------------
|
|
561
|
+
|
|
562
|
+
let currentSettings = {};
|
|
563
|
+
|
|
564
|
+
async function loadSettings() {
|
|
565
|
+
try {
|
|
566
|
+
const plugins = await api('/api/plugins');
|
|
567
|
+
const garage = plugins.find(p => p.name === 'garage');
|
|
568
|
+
currentSettings = garage?.settings || {};
|
|
569
|
+
const hasKey = !!(currentSettings.dvlaApiKey);
|
|
570
|
+
const badge = document.createElement('span');
|
|
571
|
+
badge.className = hasKey ? 'badge badge-success' : 'badge badge-warning';
|
|
572
|
+
badge.textContent = hasKey
|
|
573
|
+
? 'API key configured'
|
|
574
|
+
: 'No API key — lookups will fail until one is saved';
|
|
575
|
+
settingsKeyStatus.textContent = '';
|
|
576
|
+
settingsKeyStatus.appendChild(badge);
|
|
577
|
+
} catch (err) {
|
|
578
|
+
settingsKeyStatus.textContent = 'Could not load settings.';
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
settingsSaveBtn.addEventListener('click', async () => {
|
|
583
|
+
const key = (settingsApiKeyInput.value || '').trim();
|
|
584
|
+
if (!key) {
|
|
585
|
+
E.toast('Please enter an API key.', {type: 'warning'});
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
settingsSaveBtn.disabled = true;
|
|
590
|
+
try {
|
|
591
|
+
await api('/api/plugins/garage', 'PUT', {
|
|
592
|
+
enabled: true,
|
|
593
|
+
settings: {...currentSettings, dvlaApiKey: key}
|
|
594
|
+
});
|
|
595
|
+
currentSettings.dvlaApiKey = key;
|
|
596
|
+
settingsApiKeyInput.value = '';
|
|
597
|
+
const badge = document.createElement('span');
|
|
598
|
+
badge.className = 'badge badge-success';
|
|
599
|
+
badge.textContent = 'API key configured';
|
|
600
|
+
settingsKeyStatus.textContent = '';
|
|
601
|
+
settingsKeyStatus.appendChild(badge);
|
|
602
|
+
E.toast('Settings saved.', {type: 'success'});
|
|
603
|
+
} catch (err) {
|
|
604
|
+
E.toast('Failed to save: ' + err.message, {type: 'error'});
|
|
605
|
+
} finally {
|
|
606
|
+
settingsSaveBtn.disabled = false;
|
|
607
|
+
const icon = document.createElement('span');
|
|
608
|
+
icon.setAttribute('data-icon', 'save');
|
|
609
|
+
settingsSaveBtn.textContent = '';
|
|
610
|
+
settingsSaveBtn.appendChild(icon);
|
|
611
|
+
settingsSaveBtn.appendChild(document.createTextNode(' Save'));
|
|
612
|
+
Domma.icons.scan();
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
|
|
555
616
|
// ---------------------------------------------------------------
|
|
556
617
|
// Initial load
|
|
557
618
|
// ---------------------------------------------------------------
|
|
558
|
-
await Promise.all([loadSaved(), loadHistory()]);
|
|
619
|
+
await Promise.all([loadSaved(), loadHistory(), loadSettings()]);
|
|
559
620
|
Domma.icons.scan();
|
|
560
621
|
}
|
|
561
622
|
};
|
|
@@ -20,17 +20,8 @@
|
|
|
20
20
|
<!-- Category filter -->
|
|
21
21
|
<div id="category-filter" style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:18px;"></div>
|
|
22
22
|
|
|
23
|
-
<!-- Notes
|
|
24
|
-
<div
|
|
25
|
-
id="notes-grid"
|
|
26
|
-
style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px;"
|
|
27
|
-
></div>
|
|
28
|
-
|
|
29
|
-
<!-- Empty state -->
|
|
30
|
-
<div id="notes-empty" style="display:none;text-align:center;padding:60px 20px;opacity:0.6;">
|
|
31
|
-
<span data-icon="file-text" data-icon-size="48" style="display:block;margin-bottom:12px;"></span>
|
|
32
|
-
<p style="font-size:1rem;margin:0;">No notes yet. Click <strong>New Note</strong> to get started.</p>
|
|
33
|
-
</div>
|
|
23
|
+
<!-- Notes table -->
|
|
24
|
+
<div id="notes-table"></div>
|
|
34
25
|
|
|
35
26
|
<!-- Editor overlay -->
|
|
36
27
|
<div
|
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
// ============================================================
|
|
2
2
|
// Notes plugin admin view
|
|
3
|
-
// Uses auth-aware api() helper for all API calls
|
|
4
|
-
// Uses
|
|
5
|
-
// Security: all user-supplied content passes through escapeHtml()
|
|
6
|
-
// before being assigned to innerHTML — same pattern as todo plugin.
|
|
3
|
+
// Uses auth-aware api() helper for all API calls
|
|
4
|
+
// Uses T.create() for table rendering (Domma Table component)
|
|
7
5
|
// ============================================================
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
.replace(/"/g, '"')
|
|
16
|
-
.replace(/'/g, ''');
|
|
17
|
-
}
|
|
7
|
+
const esc = (str) => String(str ?? '')
|
|
8
|
+
.replace(/&/g, '&')
|
|
9
|
+
.replace(/</g, '<')
|
|
10
|
+
.replace(/>/g, '>')
|
|
11
|
+
.replace(/"/g, '"')
|
|
12
|
+
.replace(/'/g, ''');
|
|
18
13
|
|
|
19
|
-
/** Debounce a function call */
|
|
20
14
|
function debounce(fn, delay) {
|
|
21
15
|
let timer;
|
|
22
16
|
return (...args) => {
|
|
@@ -49,11 +43,10 @@ export const notesView = {
|
|
|
49
43
|
let categories = [];
|
|
50
44
|
let activeCategory = '';
|
|
51
45
|
let searchQuery = '';
|
|
52
|
-
let editingNoteId = null;
|
|
46
|
+
let editingNoteId = null;
|
|
47
|
+
let notesTable = null;
|
|
53
48
|
|
|
54
|
-
// ---- DOM refs
|
|
55
|
-
const gridEl = $container.find('#notes-grid').get(0);
|
|
56
|
-
const emptyEl = $container.find('#notes-empty').get(0);
|
|
49
|
+
// ---- DOM refs ----
|
|
57
50
|
const editorEl = $container.find('#note-editor').get(0);
|
|
58
51
|
const titleInput = $container.find('#note-title').get(0);
|
|
59
52
|
const contentInput = $container.find('#note-content').get(0);
|
|
@@ -86,85 +79,112 @@ export const notesView = {
|
|
|
86
79
|
}
|
|
87
80
|
}
|
|
88
81
|
|
|
89
|
-
// ---- Render
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
gridEl.textContent = '';
|
|
94
|
-
emptyEl.style.display = 'block';
|
|
82
|
+
// ---- Render table (T.create on first call, setData on subsequent) ----
|
|
83
|
+
function renderTable() {
|
|
84
|
+
if (notesTable) {
|
|
85
|
+
notesTable.setData(notes);
|
|
95
86
|
return;
|
|
96
87
|
}
|
|
97
88
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
89
|
+
notesTable = T.create('#notes-table', {
|
|
90
|
+
data: notes,
|
|
91
|
+
onRender: (el) => Domma.icons.scan(el),
|
|
92
|
+
columns: [
|
|
93
|
+
{
|
|
94
|
+
key: 'title',
|
|
95
|
+
title: 'Title',
|
|
96
|
+
sortable: true,
|
|
97
|
+
render: (v) => `<strong>${esc(v || 'Untitled')}</strong>`
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
key: 'content',
|
|
101
|
+
title: 'Content',
|
|
102
|
+
sortable: false,
|
|
103
|
+
render: (v) => {
|
|
104
|
+
const preview = (v || '').slice(0, 100);
|
|
105
|
+
const ellipsis = v && v.length > 100 ? '\u2026' : '';
|
|
106
|
+
return preview
|
|
107
|
+
? `<span style="font-size:0.87rem;opacity:0.8;">${esc(preview)}${ellipsis}</span>`
|
|
108
|
+
: '<span style="opacity:0.35;">\u2014</span>';
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
key: 'categories',
|
|
113
|
+
title: 'Categories',
|
|
114
|
+
sortable: false,
|
|
115
|
+
render: (v) => {
|
|
116
|
+
if (!Array.isArray(v) || v.length === 0) return '<span style="opacity:0.35;">\u2014</span>';
|
|
117
|
+
return v.map((c) =>
|
|
118
|
+
`<span class="badge badge-outline notes-cat-tag" data-cat="${esc(c)}" `
|
|
119
|
+
+ `style="font-size:0.7rem;margin-right:2px;cursor:pointer;">${esc(c)}</span>`
|
|
120
|
+
).join('');
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
key: 'updatedAt',
|
|
125
|
+
title: 'Updated',
|
|
126
|
+
sortable: true,
|
|
127
|
+
render: (v) => v
|
|
128
|
+
? `<span title="${esc(D(v).format('DD MMM YYYY HH:mm'))}">${esc(D(v).fromNow())}</span>`
|
|
129
|
+
: '\u2014'
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
key: 'id',
|
|
133
|
+
title: 'Actions',
|
|
134
|
+
sortable: false,
|
|
135
|
+
render: (id) =>
|
|
136
|
+
`<button class="btn btn-sm btn-outline note-edit-btn" data-id="${esc(id)}" style="margin-right:4px;">` +
|
|
137
|
+
`<span data-icon="edit" data-icon-size="13"></span> Edit</button>` +
|
|
138
|
+
`<button class="btn btn-sm btn-danger note-delete-btn" data-id="${esc(id)}">` +
|
|
139
|
+
`<span data-icon="trash" data-icon-size="13"></span></button>`
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
emptyMessage: 'No notes yet. Click New Note to get started.'
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Event delegation on the table container
|
|
146
|
+
const tableContainer = $container.find('#notes-table').get(0);
|
|
147
|
+
tableContainer.addEventListener('click', (e) => {
|
|
148
|
+
const editBtn = e.target.closest('.note-edit-btn');
|
|
149
|
+
const deleteBtn = e.target.closest('.note-delete-btn');
|
|
150
|
+
const catTag = e.target.closest('.notes-cat-tag');
|
|
151
|
+
|
|
152
|
+
if (editBtn) {
|
|
153
|
+
const note = notes.find((n) => n.id === editBtn.dataset.id);
|
|
154
|
+
if (note) openEditor(note);
|
|
155
|
+
} else if (deleteBtn) {
|
|
156
|
+
deleteNote(deleteBtn.dataset.id);
|
|
157
|
+
} else if (catTag) {
|
|
158
|
+
activeCategory = catTag.dataset.cat;
|
|
159
|
+
renderCategoryFilter();
|
|
160
|
+
loadNotes().then(renderTable);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
143
163
|
}
|
|
144
164
|
|
|
145
|
-
// ---- Render category filter bar ----
|
|
146
|
-
// Safe: all category strings are run through escapeHtml().
|
|
165
|
+
// ---- Render category filter bar (DOM methods — no innerHTML with user data) ----
|
|
147
166
|
function renderCategoryFilter() {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
167
|
+
catFilterEl.textContent = '';
|
|
168
|
+
|
|
169
|
+
const makeBtn = (label, cat) => {
|
|
170
|
+
const btn = document.createElement('button');
|
|
171
|
+
btn.className = 'btn btn-sm cat-filter-btn ' + (activeCategory === cat ? 'btn-primary' : 'btn-secondary');
|
|
172
|
+
btn.dataset.cat = cat;
|
|
173
|
+
btn.style.marginRight = '4px';
|
|
174
|
+
btn.textContent = label;
|
|
175
|
+
return btn;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
catFilterEl.appendChild(makeBtn('All', ''));
|
|
152
179
|
for (const c of categories) {
|
|
153
|
-
|
|
154
|
-
const isActive = activeCategory === c;
|
|
155
|
-
html += '<button class="btn btn-sm ' + (isActive ? 'btn-primary' : 'btn-secondary')
|
|
156
|
-
+ ' cat-filter-btn" data-cat="' + safeC + '" style="margin-right:4px;">'
|
|
157
|
-
+ safeC + '</button>';
|
|
180
|
+
catFilterEl.appendChild(makeBtn(c, c));
|
|
158
181
|
}
|
|
159
|
-
|
|
160
|
-
// All values in `html` have been escaped — safe assignment.
|
|
161
|
-
catFilterEl.innerHTML = html;
|
|
162
182
|
}
|
|
163
183
|
|
|
164
184
|
// ---- Full reload ----
|
|
165
185
|
async function reload() {
|
|
166
186
|
await Promise.all([loadNotes(), loadCategories()]);
|
|
167
|
-
|
|
187
|
+
renderTable();
|
|
168
188
|
renderCategoryFilter();
|
|
169
189
|
}
|
|
170
190
|
|
|
@@ -188,10 +208,7 @@ export const notesView = {
|
|
|
188
208
|
}
|
|
189
209
|
|
|
190
210
|
function parseCategoriesInput() {
|
|
191
|
-
return catsInput.value
|
|
192
|
-
.split(',')
|
|
193
|
-
.map((c) => c.trim())
|
|
194
|
-
.filter(Boolean);
|
|
211
|
+
return catsInput.value.split(',').map((c) => c.trim()).filter(Boolean);
|
|
195
212
|
}
|
|
196
213
|
|
|
197
214
|
// ---- Save (create or update) ----
|
|
@@ -232,69 +249,31 @@ export const notesView = {
|
|
|
232
249
|
|
|
233
250
|
// ---- Event bindings ----
|
|
234
251
|
|
|
235
|
-
// New note button
|
|
236
252
|
$container.find('#new-note-btn').get(0).addEventListener('click', () => openEditor());
|
|
237
|
-
|
|
238
|
-
// Save button
|
|
239
253
|
$container.find('#save-note-btn').get(0).addEventListener('click', saveNote);
|
|
240
|
-
|
|
241
|
-
// Cancel buttons (header X and footer Cancel both call closeEditor)
|
|
242
254
|
$container.find('#cancel-note-btn').get(0).addEventListener('click', closeEditor);
|
|
243
255
|
$container.find('#cancel-note-btn-footer').get(0).addEventListener('click', closeEditor);
|
|
244
|
-
|
|
245
|
-
// Delete button (inside editor)
|
|
246
256
|
deleteBtnEl.addEventListener('click', () => {
|
|
247
257
|
if (editingNoteId) deleteNote(editingNoteId);
|
|
248
258
|
});
|
|
249
259
|
|
|
250
|
-
// Search input (debounced)
|
|
251
260
|
$container.find('#notes-search').get(0).addEventListener('input', debounce(async (e) => {
|
|
252
261
|
searchQuery = e.target.value.trim();
|
|
253
262
|
await loadNotes();
|
|
254
|
-
|
|
263
|
+
renderTable();
|
|
255
264
|
}, 300));
|
|
256
265
|
|
|
257
|
-
// Category filter — native delegation on catFilterEl
|
|
258
266
|
catFilterEl.addEventListener('click', async (e) => {
|
|
259
267
|
const btn = e.target.closest('.cat-filter-btn');
|
|
260
268
|
if (!btn) return;
|
|
261
269
|
activeCategory = btn.dataset.cat;
|
|
262
270
|
renderCategoryFilter();
|
|
263
271
|
await loadNotes();
|
|
264
|
-
|
|
272
|
+
renderTable();
|
|
265
273
|
});
|
|
266
274
|
|
|
267
|
-
// Notes grid — native delegation
|
|
268
|
-
gridEl.addEventListener('click', (e) => {
|
|
269
|
-
// Delete button on card
|
|
270
|
-
const deleteBtn = e.target.closest('.delete-note-card');
|
|
271
|
-
if (deleteBtn) {
|
|
272
|
-
deleteNote(deleteBtn.dataset.id);
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Category tag — filter by that category
|
|
277
|
-
const catTag = e.target.closest('.notes-cat-tag');
|
|
278
|
-
if (catTag) {
|
|
279
|
-
activeCategory = catTag.dataset.cat;
|
|
280
|
-
renderCategoryFilter();
|
|
281
|
-
loadNotes().then(renderGrid);
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Click anywhere else on card — open editor
|
|
286
|
-
const card = e.target.closest('.notes-card');
|
|
287
|
-
if (card) {
|
|
288
|
-
const note = notes.find((n) => n.id === card.dataset.id);
|
|
289
|
-
if (note) openEditor(note);
|
|
290
|
-
}
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
// Ctrl+Enter / Cmd+Enter to save from editor
|
|
294
275
|
editorEl.addEventListener('keydown', (e) => {
|
|
295
|
-
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter')
|
|
296
|
-
saveNote();
|
|
297
|
-
}
|
|
276
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') saveNote();
|
|
298
277
|
});
|
|
299
278
|
|
|
300
279
|
// ---- Initial load ----
|