alchemy-widget 0.3.0-alpha.2 → 0.3.0
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/CHANGELOG.md +10 -0
- package/CLAUDE.md +160 -0
- package/assets/stylesheets/widget_picker.scss +283 -0
- package/element/20-add_area_element.js +19 -21
- package/element/30-base_toolbar_element.js +189 -2
- package/element/editor_toolbar_element.js +121 -1
- package/element/user_avatar_group_element.js +5 -0
- package/element/widget_picker_element.js +333 -0
- package/element/widget_toolbar_element.js +22 -137
- package/helper/document_watcher.js +41 -6
- package/helper/editor_toolbar_manager.js +112 -20
- package/helper/widgets/00-widget.js +239 -0
- package/helper/widgets/05-column.js +4 -0
- package/helper/widgets/05-list.js +4 -0
- package/helper/widgets/05-row.js +4 -0
- package/helper/widgets/alchemy_field_widget.js +5 -0
- package/helper/widgets/alchemy_form_widget.js +5 -0
- package/helper/widgets/alchemy_table_widget.js +4 -0
- package/helper/widgets/alchemy_tabs_widget.js +5 -0
- package/helper/widgets/hawkejs_template.js +4 -0
- package/helper/widgets/header.js +4 -0
- package/helper/widgets/html.js +4 -0
- package/helper/widgets/markdown.js +4 -0
- package/helper/widgets/sourcecode.js +4 -0
- package/helper/widgets/table_of_contents.js +4 -0
- package/helper/widgets/text.js +4 -0
- package/package.json +2 -2
- package/view/widget/elements/al_editor_toolbar.hwk +49 -1
- package/view/widget/elements/al_widget_toolbar.hwk +1 -1
- package/view/widget/elements/widget_picker.hwk +55 -0
|
@@ -65,6 +65,10 @@ Toolbar.addElementGetter('watchers_element', '.watchers');
|
|
|
65
65
|
Toolbar.setMethod(function onToolbarManagerAssignment(manager, old_manager) {
|
|
66
66
|
if (manager != old_manager) {
|
|
67
67
|
this.prepareToolbarManager(manager, old_manager);
|
|
68
|
+
} else if (manager) {
|
|
69
|
+
// Same instance re-assigned - state may have changed
|
|
70
|
+
// Re-attach the document watcher to pick up any new viewers
|
|
71
|
+
this.attachDocumentWatcher(manager.state?.document_watcher);
|
|
68
72
|
}
|
|
69
73
|
});
|
|
70
74
|
|
|
@@ -97,6 +101,15 @@ Toolbar.setMethod(function prepareToolbarManager(manager, old_manager) {
|
|
|
97
101
|
return;
|
|
98
102
|
}
|
|
99
103
|
|
|
104
|
+
// Listen for the manager being replaced (e.g., after server restart and reconnection)
|
|
105
|
+
manager.on('replaced', (new_manager) => {
|
|
106
|
+
if (new_manager && new_manager !== manager) {
|
|
107
|
+
// Clear the PREPARED flag so the new manager can be prepared
|
|
108
|
+
new_manager[PREPARED] = false;
|
|
109
|
+
this.prepareToolbarManager(new_manager, manager);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
100
113
|
let clear_counts = {};
|
|
101
114
|
|
|
102
115
|
manager.watchProperty('document_watcher', watcher => {
|
|
@@ -195,10 +208,18 @@ Toolbar.setMethod(function attachDocumentWatcher(watcher) {
|
|
|
195
208
|
|
|
196
209
|
if (!viewer.info) {
|
|
197
210
|
// The info isn't always set due to race conditions
|
|
198
|
-
|
|
211
|
+
try {
|
|
212
|
+
viewer.info = await watcher.getUserInfo(viewer.user_id);
|
|
213
|
+
} catch (err) {
|
|
214
|
+
// getUserInfo might fail if WebSocket is disconnected
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
199
217
|
}
|
|
200
218
|
|
|
201
|
-
|
|
219
|
+
// Only push if we have valid info
|
|
220
|
+
if (viewer.info) {
|
|
221
|
+
users.push(viewer.info);
|
|
222
|
+
}
|
|
202
223
|
}
|
|
203
224
|
}
|
|
204
225
|
|
|
@@ -229,3 +250,169 @@ Toolbar.setMethod(function getAreaElement(area) {
|
|
|
229
250
|
|
|
230
251
|
return this.querySelector('[data-area="' + area + '"]');
|
|
231
252
|
});
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get the widget containers to edit.
|
|
256
|
+
* Child classes should override this.
|
|
257
|
+
*
|
|
258
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
259
|
+
* @since 0.3.0
|
|
260
|
+
* @version 0.3.0
|
|
261
|
+
*
|
|
262
|
+
* @returns {Array} Array of widget elements
|
|
263
|
+
*/
|
|
264
|
+
Toolbar.setMethod(function getTargetWidgets() {
|
|
265
|
+
return [];
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Start editing the target widgets
|
|
270
|
+
*
|
|
271
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
272
|
+
* @since 0.3.0
|
|
273
|
+
* @version 0.3.0
|
|
274
|
+
*/
|
|
275
|
+
Toolbar.setMethod(function startEditing() {
|
|
276
|
+
|
|
277
|
+
let elements = this.getTargetWidgets();
|
|
278
|
+
|
|
279
|
+
if (!elements.length) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
document.body.classList.add('editing-blocks');
|
|
284
|
+
|
|
285
|
+
for (let element of elements) {
|
|
286
|
+
element.startEditor();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
this.setState('editing');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Stop editing the target widgets
|
|
294
|
+
*
|
|
295
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
296
|
+
* @since 0.3.0
|
|
297
|
+
* @version 0.3.0
|
|
298
|
+
*/
|
|
299
|
+
Toolbar.setMethod(function stopEditing() {
|
|
300
|
+
|
|
301
|
+
let elements = this.getTargetWidgets();
|
|
302
|
+
|
|
303
|
+
document.body.classList.remove('editing-blocks');
|
|
304
|
+
|
|
305
|
+
for (let element of elements) {
|
|
306
|
+
element.stopEditor();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
this.setState('ready');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Save all the target widgets
|
|
314
|
+
*
|
|
315
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
316
|
+
* @since 0.3.0
|
|
317
|
+
* @version 0.3.0
|
|
318
|
+
*/
|
|
319
|
+
Toolbar.setMethod(async function saveAll() {
|
|
320
|
+
|
|
321
|
+
if (this._saving) {
|
|
322
|
+
try {
|
|
323
|
+
await this._saving;
|
|
324
|
+
} catch (err) {
|
|
325
|
+
// Ignore
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
this._saving = null;
|
|
330
|
+
|
|
331
|
+
let elements = this.getTargetWidgets();
|
|
332
|
+
let widget_data = [];
|
|
333
|
+
let pledge;
|
|
334
|
+
|
|
335
|
+
for (let element of elements) {
|
|
336
|
+
let entry = element.gatherSaveData();
|
|
337
|
+
if (entry) {
|
|
338
|
+
widget_data.push(entry);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (widget_data.length) {
|
|
343
|
+
let config = {
|
|
344
|
+
href: alchemy.routeUrl('AlchemyWidgets#save'),
|
|
345
|
+
post: {
|
|
346
|
+
widgets: widget_data
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
pledge = alchemy.fetch(config);
|
|
351
|
+
this._saving = pledge;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return pledge;
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Save all and update button states
|
|
359
|
+
*
|
|
360
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
361
|
+
* @since 0.3.0
|
|
362
|
+
* @version 0.3.0
|
|
363
|
+
*
|
|
364
|
+
* @param {Boolean} before_stop Whether this is before stopping editing
|
|
365
|
+
*/
|
|
366
|
+
Toolbar.setMethod(async function saveAllAndUpdateButtonStates(before_stop) {
|
|
367
|
+
|
|
368
|
+
let state = 'saving',
|
|
369
|
+
button;
|
|
370
|
+
|
|
371
|
+
if (before_stop) {
|
|
372
|
+
button = this.button_stop_and_save;
|
|
373
|
+
state += '-before-stop';
|
|
374
|
+
} else {
|
|
375
|
+
button = this.button_save_all;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (!button) {
|
|
379
|
+
// No button available, just save
|
|
380
|
+
return this.saveAll();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
this.setState(state);
|
|
384
|
+
button.setState(state);
|
|
385
|
+
|
|
386
|
+
let save_error = null;
|
|
387
|
+
|
|
388
|
+
let restore_toolbar_state = this.wrapForCurrentState(() => {
|
|
389
|
+
if (save_error) {
|
|
390
|
+
this.setState('error');
|
|
391
|
+
} else {
|
|
392
|
+
this.setState('editing');
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
let restore_button_state = button.wrapForCurrentState(() => {
|
|
397
|
+
if (save_error) {
|
|
398
|
+
button.setState('error');
|
|
399
|
+
} else {
|
|
400
|
+
button.setState('saved', 2500, 'ready');
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
await this.saveAll();
|
|
406
|
+
} catch (err) {
|
|
407
|
+
save_error = err;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
restore_toolbar_state();
|
|
411
|
+
restore_button_state();
|
|
412
|
+
|
|
413
|
+
if (save_error) {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return true;
|
|
418
|
+
});
|
|
@@ -16,15 +16,135 @@ let Toolbar = Function.inherits('Alchemy.Element.Widget.BaseToolbar', 'EditorToo
|
|
|
16
16
|
*/
|
|
17
17
|
Toolbar.setTemplateFile('widget/elements/al_editor_toolbar');
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The start-edit button
|
|
21
|
+
*
|
|
22
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
23
|
+
* @since 0.3.0
|
|
24
|
+
* @version 0.3.0
|
|
25
|
+
*/
|
|
26
|
+
Toolbar.addElementGetter('button_start', 'al-button.start-edit');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The stop-edit button
|
|
30
|
+
*
|
|
31
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
32
|
+
* @since 0.3.0
|
|
33
|
+
* @version 0.3.0
|
|
34
|
+
*/
|
|
35
|
+
Toolbar.addElementGetter('button_stop', 'al-button.stop-edit');
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The stop-and-save button
|
|
39
|
+
*
|
|
40
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
41
|
+
* @since 0.3.0
|
|
42
|
+
* @version 0.3.0
|
|
43
|
+
*/
|
|
44
|
+
Toolbar.addElementGetter('button_stop_and_save', 'al-button.stop-and-save');
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The save-all button
|
|
48
|
+
*
|
|
49
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
50
|
+
* @since 0.3.0
|
|
51
|
+
* @version 0.3.0
|
|
52
|
+
*/
|
|
53
|
+
Toolbar.addElementGetter('button_save_all', 'al-button.save-all');
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get the target widgets - only those matching the toolbar manager's document
|
|
57
|
+
*
|
|
58
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
59
|
+
* @since 0.3.0
|
|
60
|
+
* @version 0.3.0
|
|
61
|
+
*/
|
|
62
|
+
Toolbar.setMethod(function getTargetWidgets() {
|
|
63
|
+
|
|
64
|
+
let manager = this.toolbar_manager;
|
|
65
|
+
|
|
66
|
+
if (!manager) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Get the document from the manager
|
|
71
|
+
let doc = manager.state?.document_watcher;
|
|
72
|
+
|
|
73
|
+
if (!doc) {
|
|
74
|
+
// No document set, return empty
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let doc_pk = doc.state?.pk;
|
|
79
|
+
|
|
80
|
+
if (!doc_pk) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Find all al-widgets elements and filter to those with matching record
|
|
85
|
+
let all_widgets = document.querySelectorAll('al-widgets');
|
|
86
|
+
let result = [];
|
|
87
|
+
|
|
88
|
+
for (let widget of all_widgets) {
|
|
89
|
+
if (widget.record && widget.record.$pk == doc_pk) {
|
|
90
|
+
result.push(widget);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return result;
|
|
95
|
+
});
|
|
96
|
+
|
|
19
97
|
/**
|
|
20
98
|
* Added to the dom for the first time
|
|
21
99
|
*
|
|
22
100
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
23
101
|
* @since 0.2.7
|
|
24
|
-
* @version 0.
|
|
102
|
+
* @version 0.3.0
|
|
25
103
|
*/
|
|
26
104
|
Toolbar.setMethod(async function introduced() {
|
|
27
105
|
|
|
28
106
|
this.prepareToolbarManager(this.toolbar_manager);
|
|
29
107
|
|
|
108
|
+
// Set up button event listeners
|
|
109
|
+
if (this.button_start) {
|
|
110
|
+
this.button_start.addEventListener('activate', async e => {
|
|
111
|
+
this.startEditing();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (this.button_stop) {
|
|
116
|
+
this.button_stop.addEventListener('activate', async e => {
|
|
117
|
+
this.stopEditing();
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (this.button_save_all) {
|
|
122
|
+
this.button_save_all.addEventListener('activate', e => {
|
|
123
|
+
this.saveAllAndUpdateButtonStates(false);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (this.button_stop_and_save) {
|
|
128
|
+
this.button_stop_and_save.addEventListener('activate', async e => {
|
|
129
|
+
let saved = await this.saveAllAndUpdateButtonStates(true);
|
|
130
|
+
if (saved) {
|
|
131
|
+
this.stopEditing();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Handle new toolbar manager from server during client-side navigation
|
|
137
|
+
hawkejs.scene.on('rendered', (variables, renderer) => {
|
|
138
|
+
if (variables.toolbar_manager) {
|
|
139
|
+
this.prepareToolbarManager(variables.toolbar_manager, this.toolbar_manager);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Handle navigation - stop editing when leaving the page
|
|
144
|
+
hawkejs.scene.on('opening_url', (href, options) => {
|
|
145
|
+
if (options?.history === false) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
this.stopEditing();
|
|
149
|
+
});
|
|
30
150
|
});
|
|
@@ -70,6 +70,11 @@ UserAvatarGroup.setMethod(function setUsers(users) {
|
|
|
70
70
|
for (let i = 0; i < users.length; i++) {
|
|
71
71
|
user = users[i];
|
|
72
72
|
|
|
73
|
+
// Skip null/undefined users
|
|
74
|
+
if (!user) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
73
78
|
updated = false;
|
|
74
79
|
|
|
75
80
|
for (let j = 0; j < existing_avatars.length; j++) {
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The widget picker element
|
|
3
|
+
* A Notion-style dialog for selecting widgets to add
|
|
4
|
+
*
|
|
5
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
6
|
+
* @since 0.3.0
|
|
7
|
+
* @version 0.3.0
|
|
8
|
+
*/
|
|
9
|
+
const WidgetPicker = Function.inherits('Alchemy.Element.Widget.Base', 'WidgetPicker');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The template to use
|
|
13
|
+
*/
|
|
14
|
+
WidgetPicker.setTemplateFile('widget/elements/widget_picker');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The stylesheet to use
|
|
18
|
+
*/
|
|
19
|
+
WidgetPicker.setStylesheetFile('widget_picker');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The container element that will receive the selected widget
|
|
23
|
+
*/
|
|
24
|
+
WidgetPicker.setAssignedProperty('target_container');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Search query attribute
|
|
28
|
+
*/
|
|
29
|
+
WidgetPicker.setAttribute('search');
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Currently selected category filter
|
|
33
|
+
*/
|
|
34
|
+
WidgetPicker.setAttribute('category');
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get available widgets that can be added to the target container
|
|
38
|
+
*
|
|
39
|
+
* @return {Array} Array of widget info objects
|
|
40
|
+
*/
|
|
41
|
+
WidgetPicker.setMethod(async function getAvailableWidgets() {
|
|
42
|
+
|
|
43
|
+
const Widget = Classes.Alchemy.Widget.Widget;
|
|
44
|
+
const widgets = Object.values(alchemy.getClassGroup('widgets'));
|
|
45
|
+
const container = this.target_container;
|
|
46
|
+
const results = [];
|
|
47
|
+
|
|
48
|
+
for (let widget of widgets) {
|
|
49
|
+
// Check if widget can be added to this container
|
|
50
|
+
let canAdd = widget.canBeAdded(container);
|
|
51
|
+
|
|
52
|
+
if (Pledge.isThenable(canAdd)) {
|
|
53
|
+
canAdd = await canAdd;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!canAdd) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
results.push({
|
|
61
|
+
type_name: widget.type_name,
|
|
62
|
+
title: widget.title || widget.type_name.titleize(),
|
|
63
|
+
description: widget.description || null, // Hardcoded description takes precedence
|
|
64
|
+
description_key: 'widget-' + widget.type_name + '-description',
|
|
65
|
+
icon: widget.icon || 'puzzle-piece',
|
|
66
|
+
category: widget.category || 'advanced',
|
|
67
|
+
categoryInfo: Widget.getCategoryInfo(widget.category)
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Sort by title
|
|
72
|
+
return results.sortByPath(1, 'title');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get all categories that have widgets
|
|
77
|
+
*
|
|
78
|
+
* @param {Array} widgets Optional array of widgets (to avoid re-fetching)
|
|
79
|
+
*
|
|
80
|
+
* @return {Array} Array of category info objects
|
|
81
|
+
*/
|
|
82
|
+
WidgetPicker.setMethod(function getUsedCategories(widgets) {
|
|
83
|
+
|
|
84
|
+
if (!widgets) {
|
|
85
|
+
widgets = this.available_widgets || [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const Widget = Classes.Alchemy.Widget.Widget;
|
|
89
|
+
const categoryMap = new Map();
|
|
90
|
+
|
|
91
|
+
for (let widget of widgets) {
|
|
92
|
+
if (!categoryMap.has(widget.category)) {
|
|
93
|
+
categoryMap.set(widget.category, Widget.getCategoryInfo(widget.category));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Convert to array and sort by order
|
|
98
|
+
return Array.from(categoryMap.values()).sortByPath(1, 'order');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Prepare render variables
|
|
103
|
+
*/
|
|
104
|
+
WidgetPicker.setMethod(async function prepareRenderVariables() {
|
|
105
|
+
|
|
106
|
+
const widgets = await this.getAvailableWidgets();
|
|
107
|
+
const categories = this.getUsedCategories(widgets);
|
|
108
|
+
|
|
109
|
+
// Group widgets by category
|
|
110
|
+
const widgetsByCategory = {};
|
|
111
|
+
for (let widget of widgets) {
|
|
112
|
+
const cat = widget.category || 'advanced';
|
|
113
|
+
if (!widgetsByCategory[cat]) {
|
|
114
|
+
widgetsByCategory[cat] = [];
|
|
115
|
+
}
|
|
116
|
+
widgetsByCategory[cat].push(widget);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Attach widgets directly to each category object
|
|
120
|
+
// This avoids bracket notation issues in templates
|
|
121
|
+
for (let category of categories) {
|
|
122
|
+
category.widgets = widgetsByCategory[category.name] || [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
widgets,
|
|
127
|
+
categories
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Filter widgets based on search and category
|
|
133
|
+
*/
|
|
134
|
+
WidgetPicker.setMethod(function filterWidgets() {
|
|
135
|
+
|
|
136
|
+
const search = (this.search || '').toLowerCase().trim();
|
|
137
|
+
const category = this.category;
|
|
138
|
+
const items = this.querySelectorAll('.widget-item');
|
|
139
|
+
const categoryGroups = this.querySelectorAll('.widget-category-group');
|
|
140
|
+
|
|
141
|
+
// Track which categories have visible items
|
|
142
|
+
const visibleCategories = new Set();
|
|
143
|
+
|
|
144
|
+
items.forEach(item => {
|
|
145
|
+
const title = (item.dataset.title || '').toLowerCase();
|
|
146
|
+
const desc = (item.dataset.description || '').toLowerCase();
|
|
147
|
+
const itemCategory = item.dataset.category;
|
|
148
|
+
|
|
149
|
+
const matchesSearch = !search ||
|
|
150
|
+
title.includes(search) ||
|
|
151
|
+
desc.includes(search);
|
|
152
|
+
const matchesCategory = !category || itemCategory === category;
|
|
153
|
+
|
|
154
|
+
const visible = matchesSearch && matchesCategory;
|
|
155
|
+
item.hidden = !visible;
|
|
156
|
+
|
|
157
|
+
if (visible) {
|
|
158
|
+
visibleCategories.add(itemCategory);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Show/hide category groups based on whether they have visible items
|
|
163
|
+
categoryGroups.forEach(group => {
|
|
164
|
+
const groupCategory = group.dataset.category;
|
|
165
|
+
group.hidden = !visibleCategories.has(groupCategory);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
this.updateEmptyState();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Update the empty state message
|
|
173
|
+
*/
|
|
174
|
+
WidgetPicker.setMethod(function updateEmptyState() {
|
|
175
|
+
|
|
176
|
+
const emptyEl = this.querySelector('.widget-picker-empty');
|
|
177
|
+
const visibleItems = this.querySelectorAll('.widget-item:not([hidden])');
|
|
178
|
+
|
|
179
|
+
if (emptyEl) {
|
|
180
|
+
emptyEl.hidden = visibleItems.length > 0;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Select a widget
|
|
186
|
+
*/
|
|
187
|
+
WidgetPicker.setMethod(function selectWidget(type_name) {
|
|
188
|
+
|
|
189
|
+
// Emit the select event with the widget type name
|
|
190
|
+
this.dispatchEvent(new CustomEvent('select', {
|
|
191
|
+
bubbles: true,
|
|
192
|
+
detail: {type_name}
|
|
193
|
+
}));
|
|
194
|
+
|
|
195
|
+
// Close the dialog
|
|
196
|
+
const dialog = this.queryParents('he-dialog');
|
|
197
|
+
if (dialog) {
|
|
198
|
+
dialog.close();
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Set up the search input
|
|
204
|
+
*/
|
|
205
|
+
WidgetPicker.setMethod(function setupSearch() {
|
|
206
|
+
|
|
207
|
+
const input = this.querySelector('.widget-picker-search');
|
|
208
|
+
|
|
209
|
+
if (!input) return;
|
|
210
|
+
|
|
211
|
+
input.addEventListener('input', (e) => {
|
|
212
|
+
this.search = e.target.value;
|
|
213
|
+
this.filterWidgets();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Focus search on open
|
|
217
|
+
requestAnimationFrame(() => input.focus());
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Set up category filter buttons
|
|
222
|
+
*/
|
|
223
|
+
WidgetPicker.setMethod(function setupCategoryFilters() {
|
|
224
|
+
|
|
225
|
+
const buttons = this.querySelectorAll('.category-btn');
|
|
226
|
+
|
|
227
|
+
buttons.forEach(btn => {
|
|
228
|
+
btn.addEventListener('click', (e) => {
|
|
229
|
+
e.preventDefault();
|
|
230
|
+
|
|
231
|
+
// Update active state
|
|
232
|
+
buttons.forEach(b => b.classList.remove('active'));
|
|
233
|
+
btn.classList.add('active');
|
|
234
|
+
|
|
235
|
+
// Set category filter
|
|
236
|
+
this.category = btn.dataset.category || '';
|
|
237
|
+
this.filterWidgets();
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Set up widget item click handlers
|
|
244
|
+
*/
|
|
245
|
+
WidgetPicker.setMethod(function setupWidgetItems() {
|
|
246
|
+
|
|
247
|
+
const items = this.querySelectorAll('.widget-item');
|
|
248
|
+
|
|
249
|
+
items.forEach(item => {
|
|
250
|
+
item.addEventListener('click', (e) => {
|
|
251
|
+
e.preventDefault();
|
|
252
|
+
this.selectWidget(item.dataset.type);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Set up keyboard navigation
|
|
259
|
+
*/
|
|
260
|
+
WidgetPicker.setMethod(function setupKeyboard() {
|
|
261
|
+
|
|
262
|
+
this.addEventListener('keydown', (e) => {
|
|
263
|
+
if (e.key === 'Escape') {
|
|
264
|
+
const dialog = this.queryParents('he-dialog');
|
|
265
|
+
if (dialog) {
|
|
266
|
+
dialog.close();
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (e.key === 'Enter') {
|
|
272
|
+
const focused = this.querySelector('.widget-item:focus');
|
|
273
|
+
if (focused) {
|
|
274
|
+
e.preventDefault();
|
|
275
|
+
this.selectWidget(focused.dataset.type);
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
281
|
+
e.preventDefault();
|
|
282
|
+
this.navigateItems(e.key === 'ArrowDown' ? 1 : -1);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Navigate through widget items with arrow keys
|
|
289
|
+
*/
|
|
290
|
+
WidgetPicker.setMethod(function navigateItems(direction) {
|
|
291
|
+
|
|
292
|
+
const items = Array.from(this.querySelectorAll('.widget-item:not([hidden])'));
|
|
293
|
+
if (!items.length) return;
|
|
294
|
+
|
|
295
|
+
const focused = this.querySelector('.widget-item:focus');
|
|
296
|
+
let index = focused ? items.indexOf(focused) : -1;
|
|
297
|
+
|
|
298
|
+
index += direction;
|
|
299
|
+
|
|
300
|
+
if (index < 0) index = items.length - 1;
|
|
301
|
+
if (index >= items.length) index = 0;
|
|
302
|
+
|
|
303
|
+
items[index].focus();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Show loading state when element is first connected to the DOM
|
|
308
|
+
*/
|
|
309
|
+
WidgetPicker.setMethod(function connected() {
|
|
310
|
+
|
|
311
|
+
// Guard against re-entry if element is moved in DOM
|
|
312
|
+
if (this._initialized) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
this._initialized = true;
|
|
317
|
+
|
|
318
|
+
// Show loading state immediately while prepareRenderVariables() runs
|
|
319
|
+
this.innerHTML = '<div class="widget-picker-loading"><div class="spinner"></div><p>Loading widgets...</p></div>';
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Added to the DOM for the first time
|
|
324
|
+
*/
|
|
325
|
+
WidgetPicker.setMethod(function introduced() {
|
|
326
|
+
|
|
327
|
+
introduced.super.call(this);
|
|
328
|
+
|
|
329
|
+
this.setupSearch();
|
|
330
|
+
this.setupCategoryFilters();
|
|
331
|
+
this.setupWidgetItems();
|
|
332
|
+
this.setupKeyboard();
|
|
333
|
+
});
|