domma-cms 0.9.1 → 0.9.5
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 +19 -10
- package/admin/js/views/view-editor.js +1 -1
- package/config/plugins.json +35 -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 +107 -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 +122 -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,111 @@ 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
|
+
columns: [
|
|
92
|
+
{
|
|
93
|
+
key: 'title',
|
|
94
|
+
title: 'Title',
|
|
95
|
+
sortable: true,
|
|
96
|
+
render: (v) => `<strong>${esc(v || 'Untitled')}</strong>`
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
key: 'content',
|
|
100
|
+
title: 'Content',
|
|
101
|
+
sortable: false,
|
|
102
|
+
render: (v) => {
|
|
103
|
+
const preview = (v || '').slice(0, 100);
|
|
104
|
+
const ellipsis = v && v.length > 100 ? '\u2026' : '';
|
|
105
|
+
return preview
|
|
106
|
+
? `<span style="font-size:0.87rem;opacity:0.8;">${esc(preview)}${ellipsis}</span>`
|
|
107
|
+
: '<span style="opacity:0.35;">\u2014</span>';
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
key: 'categories',
|
|
112
|
+
title: 'Categories',
|
|
113
|
+
sortable: false,
|
|
114
|
+
render: (v) => {
|
|
115
|
+
if (!Array.isArray(v) || v.length === 0) return '<span style="opacity:0.35;">\u2014</span>';
|
|
116
|
+
return v.map((c) =>
|
|
117
|
+
`<span class="badge badge-outline notes-cat-tag" data-cat="${esc(c)}" `
|
|
118
|
+
+ `style="font-size:0.7rem;margin-right:2px;cursor:pointer;">${esc(c)}</span>`
|
|
119
|
+
).join('');
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
key: 'updatedAt',
|
|
124
|
+
title: 'Updated',
|
|
125
|
+
sortable: true,
|
|
126
|
+
render: (v) => v
|
|
127
|
+
? `<span title="${esc(D(v).format('DD MMM YYYY HH:mm'))}">${esc(D(v).fromNow())}</span>`
|
|
128
|
+
: '\u2014'
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
key: 'id',
|
|
132
|
+
title: 'Actions',
|
|
133
|
+
sortable: false,
|
|
134
|
+
render: (id) =>
|
|
135
|
+
`<button class="btn btn-sm btn-outline note-edit-btn" data-id="${esc(id)}" style="margin-right:4px;">` +
|
|
136
|
+
`<span data-icon="edit" data-icon-size="13"></span> Edit</button>` +
|
|
137
|
+
`<button class="btn btn-sm btn-danger note-delete-btn" data-id="${esc(id)}">` +
|
|
138
|
+
`<span data-icon="trash" data-icon-size="13"></span></button>`
|
|
139
|
+
}
|
|
140
|
+
],
|
|
141
|
+
emptyMessage: 'No notes yet. Click New Note to get started.'
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Event delegation on the table container
|
|
145
|
+
const tableContainer = $container.find('#notes-table').get(0);
|
|
146
|
+
tableContainer.addEventListener('click', (e) => {
|
|
147
|
+
const editBtn = e.target.closest('.note-edit-btn');
|
|
148
|
+
const deleteBtn = e.target.closest('.note-delete-btn');
|
|
149
|
+
const catTag = e.target.closest('.notes-cat-tag');
|
|
150
|
+
|
|
151
|
+
if (editBtn) {
|
|
152
|
+
const note = notes.find((n) => n.id === editBtn.dataset.id);
|
|
153
|
+
if (note) openEditor(note);
|
|
154
|
+
} else if (deleteBtn) {
|
|
155
|
+
deleteNote(deleteBtn.dataset.id);
|
|
156
|
+
} else if (catTag) {
|
|
157
|
+
activeCategory = catTag.dataset.cat;
|
|
158
|
+
renderCategoryFilter();
|
|
159
|
+
loadNotes().then(renderTable);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
143
162
|
}
|
|
144
163
|
|
|
145
|
-
// ---- Render category filter bar ----
|
|
146
|
-
// Safe: all category strings are run through escapeHtml().
|
|
164
|
+
// ---- Render category filter bar (DOM methods — no innerHTML with user data) ----
|
|
147
165
|
function renderCategoryFilter() {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
166
|
+
catFilterEl.textContent = '';
|
|
167
|
+
|
|
168
|
+
const makeBtn = (label, cat) => {
|
|
169
|
+
const btn = document.createElement('button');
|
|
170
|
+
btn.className = 'btn btn-sm cat-filter-btn ' + (activeCategory === cat ? 'btn-primary' : 'btn-secondary');
|
|
171
|
+
btn.dataset.cat = cat;
|
|
172
|
+
btn.style.marginRight = '4px';
|
|
173
|
+
btn.textContent = label;
|
|
174
|
+
return btn;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
catFilterEl.appendChild(makeBtn('All', ''));
|
|
152
178
|
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>';
|
|
179
|
+
catFilterEl.appendChild(makeBtn(c, c));
|
|
158
180
|
}
|
|
159
|
-
|
|
160
|
-
// All values in `html` have been escaped — safe assignment.
|
|
161
|
-
catFilterEl.innerHTML = html;
|
|
162
181
|
}
|
|
163
182
|
|
|
164
183
|
// ---- Full reload ----
|
|
165
184
|
async function reload() {
|
|
166
185
|
await Promise.all([loadNotes(), loadCategories()]);
|
|
167
|
-
|
|
186
|
+
renderTable();
|
|
168
187
|
renderCategoryFilter();
|
|
169
188
|
}
|
|
170
189
|
|
|
@@ -188,10 +207,7 @@ export const notesView = {
|
|
|
188
207
|
}
|
|
189
208
|
|
|
190
209
|
function parseCategoriesInput() {
|
|
191
|
-
return catsInput.value
|
|
192
|
-
.split(',')
|
|
193
|
-
.map((c) => c.trim())
|
|
194
|
-
.filter(Boolean);
|
|
210
|
+
return catsInput.value.split(',').map((c) => c.trim()).filter(Boolean);
|
|
195
211
|
}
|
|
196
212
|
|
|
197
213
|
// ---- Save (create or update) ----
|
|
@@ -232,69 +248,31 @@ export const notesView = {
|
|
|
232
248
|
|
|
233
249
|
// ---- Event bindings ----
|
|
234
250
|
|
|
235
|
-
// New note button
|
|
236
251
|
$container.find('#new-note-btn').get(0).addEventListener('click', () => openEditor());
|
|
237
|
-
|
|
238
|
-
// Save button
|
|
239
252
|
$container.find('#save-note-btn').get(0).addEventListener('click', saveNote);
|
|
240
|
-
|
|
241
|
-
// Cancel buttons (header X and footer Cancel both call closeEditor)
|
|
242
253
|
$container.find('#cancel-note-btn').get(0).addEventListener('click', closeEditor);
|
|
243
254
|
$container.find('#cancel-note-btn-footer').get(0).addEventListener('click', closeEditor);
|
|
244
|
-
|
|
245
|
-
// Delete button (inside editor)
|
|
246
255
|
deleteBtnEl.addEventListener('click', () => {
|
|
247
256
|
if (editingNoteId) deleteNote(editingNoteId);
|
|
248
257
|
});
|
|
249
258
|
|
|
250
|
-
// Search input (debounced)
|
|
251
259
|
$container.find('#notes-search').get(0).addEventListener('input', debounce(async (e) => {
|
|
252
260
|
searchQuery = e.target.value.trim();
|
|
253
261
|
await loadNotes();
|
|
254
|
-
|
|
262
|
+
renderTable();
|
|
255
263
|
}, 300));
|
|
256
264
|
|
|
257
|
-
// Category filter — native delegation on catFilterEl
|
|
258
265
|
catFilterEl.addEventListener('click', async (e) => {
|
|
259
266
|
const btn = e.target.closest('.cat-filter-btn');
|
|
260
267
|
if (!btn) return;
|
|
261
268
|
activeCategory = btn.dataset.cat;
|
|
262
269
|
renderCategoryFilter();
|
|
263
270
|
await loadNotes();
|
|
264
|
-
|
|
271
|
+
renderTable();
|
|
265
272
|
});
|
|
266
273
|
|
|
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
274
|
editorEl.addEventListener('keydown', (e) => {
|
|
295
|
-
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter')
|
|
296
|
-
saveNote();
|
|
297
|
-
}
|
|
275
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') saveNote();
|
|
298
276
|
});
|
|
299
277
|
|
|
300
278
|
// ---- Initial load ----
|