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
|
@@ -57,7 +57,7 @@ Toolbar.addElementGetter('button_save_all', 'al-button.save-all');
|
|
|
57
57
|
*
|
|
58
58
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
59
59
|
* @since 0.2.0
|
|
60
|
-
* @version 0.
|
|
60
|
+
* @version 0.3.0
|
|
61
61
|
*/
|
|
62
62
|
Toolbar.setStatic(function show() {
|
|
63
63
|
|
|
@@ -71,6 +71,11 @@ Toolbar.setStatic(function show() {
|
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// Don't show if an al-editor-toolbar exists (e.g., in Chimera)
|
|
75
|
+
if (document.querySelector('al-editor-toolbar')) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
74
79
|
toolbar = hawkejs.createElement('al-widget-toolbar');
|
|
75
80
|
|
|
76
81
|
hawkejs.scene.bottom_element.append(toolbar);
|
|
@@ -105,27 +110,27 @@ Toolbar.setMethod(function getAllRootWidgets() {
|
|
|
105
110
|
return result;
|
|
106
111
|
});
|
|
107
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Get the target widgets (all root widgets for this toolbar)
|
|
115
|
+
*
|
|
116
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
117
|
+
* @since 0.3.0
|
|
118
|
+
* @version 0.3.0
|
|
119
|
+
*/
|
|
120
|
+
Toolbar.setMethod(function getTargetWidgets() {
|
|
121
|
+
return this.getAllRootWidgets();
|
|
122
|
+
});
|
|
123
|
+
|
|
108
124
|
/**
|
|
109
125
|
* Start editing all the widgets
|
|
110
126
|
*
|
|
111
127
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
112
128
|
* @since 0.2.0
|
|
113
|
-
* @version 0.
|
|
129
|
+
* @version 0.3.0
|
|
114
130
|
*/
|
|
115
131
|
Toolbar.setMethod(function startEditing() {
|
|
116
|
-
|
|
117
|
-
let i;
|
|
118
|
-
|
|
119
132
|
Blast.editing = true;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
let elements = this.getAllRootWidgets();
|
|
123
|
-
|
|
124
|
-
for (i = 0; i < elements.length; i++) {
|
|
125
|
-
elements[i].startEditor();
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
this.setState('editing');
|
|
133
|
+
startEditing.super.call(this);
|
|
129
134
|
});
|
|
130
135
|
|
|
131
136
|
/**
|
|
@@ -133,7 +138,7 @@ Toolbar.setMethod(function startEditing() {
|
|
|
133
138
|
*
|
|
134
139
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
135
140
|
* @since 0.2.0
|
|
136
|
-
* @version 0.
|
|
141
|
+
* @version 0.3.0
|
|
137
142
|
*/
|
|
138
143
|
Toolbar.setMethod(function stopEditing() {
|
|
139
144
|
|
|
@@ -141,123 +146,8 @@ Toolbar.setMethod(function stopEditing() {
|
|
|
141
146
|
return;
|
|
142
147
|
}
|
|
143
148
|
|
|
144
|
-
let i;
|
|
145
|
-
|
|
146
149
|
Blast.editing = false;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
let elements = this.getAllRootWidgets();
|
|
150
|
-
|
|
151
|
-
for (i = 0; i < elements.length; i++) {
|
|
152
|
-
elements[i].stopEditor();
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
this.setState('ready');
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Save all the widgets
|
|
160
|
-
*
|
|
161
|
-
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
162
|
-
* @since 0.2.0
|
|
163
|
-
* @version 0.2.0
|
|
164
|
-
*/
|
|
165
|
-
Toolbar.setMethod(async function saveAll() {
|
|
166
|
-
|
|
167
|
-
if (this._saving) {
|
|
168
|
-
try {
|
|
169
|
-
await this._saving;
|
|
170
|
-
} catch (err) {
|
|
171
|
-
// Ignore;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
this._saving = null;
|
|
176
|
-
|
|
177
|
-
let elements = this.getAllRootWidgets();
|
|
178
|
-
let widget_data = [];
|
|
179
|
-
let pledge;
|
|
180
|
-
|
|
181
|
-
for (let element of elements) {
|
|
182
|
-
let entry = element.gatherSaveData();
|
|
183
|
-
|
|
184
|
-
if (entry) {
|
|
185
|
-
widget_data.push(entry);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (widget_data.length) {
|
|
190
|
-
let config = {
|
|
191
|
-
href : alchemy.routeUrl('AlchemyWidgets#save'),
|
|
192
|
-
post : {
|
|
193
|
-
widgets: widget_data
|
|
194
|
-
}
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
pledge = alchemy.fetch(config);
|
|
198
|
-
this._saving = pledge;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return pledge;
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Save all and update the states
|
|
206
|
-
*
|
|
207
|
-
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
208
|
-
* @since 0.2.0
|
|
209
|
-
* @version 0.2.0
|
|
210
|
-
*
|
|
211
|
-
* @param {Boolean} before_stop
|
|
212
|
-
*/
|
|
213
|
-
Toolbar.setMethod(async function saveAllAndUpdateButtonStates(before_stop) {
|
|
214
|
-
|
|
215
|
-
let state = 'saving',
|
|
216
|
-
button;
|
|
217
|
-
|
|
218
|
-
if (before_stop) {
|
|
219
|
-
button = this.button_stop_and_save;
|
|
220
|
-
state += '-before-stop';
|
|
221
|
-
} else {
|
|
222
|
-
button = this.button_save_all;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
this.setState(state);
|
|
226
|
-
button.setState(state);
|
|
227
|
-
|
|
228
|
-
let save_error = null;
|
|
229
|
-
|
|
230
|
-
let restore_toolbar_state = this.wrapForCurrentState(() => {
|
|
231
|
-
if (save_error) {
|
|
232
|
-
this.setState('error');
|
|
233
|
-
} else {
|
|
234
|
-
this.setState('editing')
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
let restore_button_state = button.wrapForCurrentState(() => {
|
|
239
|
-
|
|
240
|
-
if (save_error) {
|
|
241
|
-
button.setState('error');
|
|
242
|
-
} else {
|
|
243
|
-
button.setState('saved', 2500, 'ready');
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
try {
|
|
248
|
-
await this.saveAll();
|
|
249
|
-
} catch (err) {
|
|
250
|
-
save_error = err;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
restore_toolbar_state();
|
|
254
|
-
restore_button_state();
|
|
255
|
-
|
|
256
|
-
if (save_error) {
|
|
257
|
-
return false;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return true;
|
|
150
|
+
stopEditing.super.call(this);
|
|
261
151
|
});
|
|
262
152
|
|
|
263
153
|
/**
|
|
@@ -284,11 +174,6 @@ Toolbar.setMethod(function introduced() {
|
|
|
284
174
|
|
|
285
175
|
if (this.toolbar_manager) {
|
|
286
176
|
this.prepareToolbarManager(this.toolbar_manager);
|
|
287
|
-
} else {
|
|
288
|
-
let manager = hawkejs.scene.exposed.toolbar_manager;
|
|
289
|
-
if (manager) {
|
|
290
|
-
this.prepareToolbarManager(manager);
|
|
291
|
-
}
|
|
292
177
|
}
|
|
293
178
|
|
|
294
179
|
this.button_start.addEventListener('activate', async e => {
|
|
@@ -354,4 +239,4 @@ Toolbar.setMethod(function connected() {
|
|
|
354
239
|
Toolbar.setMethod(function disconnected() {
|
|
355
240
|
let html = document.querySelector('html');
|
|
356
241
|
html.classList.remove('with-al-widget-toolbar');
|
|
357
|
-
});
|
|
242
|
+
});
|
|
@@ -24,11 +24,9 @@ const COLOURS = [
|
|
|
24
24
|
*
|
|
25
25
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
26
26
|
* @since 0.2.7
|
|
27
|
-
* @version 0.
|
|
27
|
+
* @version 0.3.0
|
|
28
28
|
*/
|
|
29
|
-
const DocumentWatcher = Function.inherits('Alchemy.Syncable', 'Alchemy.Widget',
|
|
30
|
-
DocumentWatcher.super.call(this, 'document_watcher');
|
|
31
|
-
});
|
|
29
|
+
const DocumentWatcher = Function.inherits('Alchemy.Syncable.Specialized', 'Alchemy.Widget', 'DocumentWatcher');
|
|
32
30
|
|
|
33
31
|
/**
|
|
34
32
|
* Create a watcher for the given document
|
|
@@ -60,6 +58,42 @@ DocumentWatcher.setStatic(function create(model, pk) {
|
|
|
60
58
|
|
|
61
59
|
if (Blast.isNode) {
|
|
62
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Recreate a watcher after server restart.
|
|
63
|
+
* Called by Syncable.tryRecreate() when client reconnects.
|
|
64
|
+
*
|
|
65
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
66
|
+
* @since 0.3.0
|
|
67
|
+
* @version 0.3.0
|
|
68
|
+
*
|
|
69
|
+
* @param {Conduit} conduit
|
|
70
|
+
* @param {Object} config Contains type, id, and version from the client
|
|
71
|
+
*
|
|
72
|
+
* @return {DocumentWatcher}
|
|
73
|
+
*/
|
|
74
|
+
DocumentWatcher.setStatic(async function recreate(conduit, config) {
|
|
75
|
+
|
|
76
|
+
// The config.id follows the pattern "Model:pk"
|
|
77
|
+
let parts = config.id.split(':');
|
|
78
|
+
|
|
79
|
+
if (parts.length < 2) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let model = parts[0],
|
|
84
|
+
pk = parts.slice(1).join(':'); // Handle pks with colons
|
|
85
|
+
|
|
86
|
+
// Use the existing create method which handles caching
|
|
87
|
+
let watcher = this.create(model, pk);
|
|
88
|
+
|
|
89
|
+
if (watcher) {
|
|
90
|
+
// Re-add this conduit as a watcher
|
|
91
|
+
await watcher.addWatcher(conduit);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return watcher;
|
|
95
|
+
});
|
|
96
|
+
|
|
63
97
|
/**
|
|
64
98
|
* Add a viewer based on the conduit
|
|
65
99
|
*
|
|
@@ -69,12 +103,13 @@ if (Blast.isNode) {
|
|
|
69
103
|
*
|
|
70
104
|
* @param {Alchemy.Conduit} conduit
|
|
71
105
|
*/
|
|
72
|
-
DocumentWatcher.setTypedMethod([Types.Alchemy.Conduit], function addWatcher(conduit) {
|
|
106
|
+
DocumentWatcher.setTypedMethod([Types.Alchemy.Conduit], async function addWatcher(conduit) {
|
|
73
107
|
|
|
74
108
|
let user_id = '' + conduit.getUserId(),
|
|
75
109
|
scene_id = conduit.scene_id;
|
|
76
110
|
|
|
77
|
-
|
|
111
|
+
// IMPORTANT: await the async addWatcher method!
|
|
112
|
+
await this.addWatcher(user_id, scene_id);
|
|
78
113
|
|
|
79
114
|
// Watchers should also be registered as a client
|
|
80
115
|
this.registerClient(conduit);
|
|
@@ -9,10 +9,64 @@ const SCENE_MAP = new Map(),
|
|
|
9
9
|
*
|
|
10
10
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
11
11
|
* @since 0.2.7
|
|
12
|
-
* @version 0.
|
|
12
|
+
* @version 0.3.0
|
|
13
|
+
*/
|
|
14
|
+
const EditorToolbarManager = Function.inherits('Alchemy.Syncable.Specialized', 'Alchemy.Widget', 'EditorToolbarManager');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Storage for document button providers
|
|
18
|
+
*
|
|
19
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
20
|
+
* @since 0.3.0
|
|
21
|
+
* @version 0.3.0
|
|
22
|
+
*/
|
|
23
|
+
EditorToolbarManager.setStatic('document_button_providers', []);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Storage for model button providers
|
|
27
|
+
*
|
|
28
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
29
|
+
* @since 0.3.0
|
|
30
|
+
* @version 0.3.0
|
|
31
|
+
*/
|
|
32
|
+
EditorToolbarManager.setStatic('model_button_providers', []);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Register a callback that can add toolbar buttons when a document is set.
|
|
36
|
+
* This allows plugins to add their own buttons without modifying this file.
|
|
37
|
+
*
|
|
38
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
39
|
+
* @since 0.3.0
|
|
40
|
+
* @version 0.3.0
|
|
41
|
+
*
|
|
42
|
+
* @param {Function} callback Function(manager, doc, model, model_name, pk_val)
|
|
43
|
+
*/
|
|
44
|
+
EditorToolbarManager.setStatic(function registerDocumentButtonProvider(callback) {
|
|
45
|
+
|
|
46
|
+
if (typeof callback !== 'function') {
|
|
47
|
+
throw new Error('Button provider must be a function');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.document_button_providers.push(callback);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Register a callback that can add toolbar buttons when a model is set.
|
|
55
|
+
* This allows plugins to add their own buttons without modifying this file.
|
|
56
|
+
*
|
|
57
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
58
|
+
* @since 0.3.0
|
|
59
|
+
* @version 0.3.0
|
|
60
|
+
*
|
|
61
|
+
* @param {Function} callback Function(manager, model_name)
|
|
13
62
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
63
|
+
EditorToolbarManager.setStatic(function registerModelButtonProvider(callback) {
|
|
64
|
+
|
|
65
|
+
if (typeof callback !== 'function') {
|
|
66
|
+
throw new Error('Button provider must be a function');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.model_button_providers.push(callback);
|
|
16
70
|
});
|
|
17
71
|
|
|
18
72
|
if (Blast.isNode) {
|
|
@@ -61,6 +115,36 @@ if (Blast.isNode) {
|
|
|
61
115
|
|
|
62
116
|
return manager;
|
|
63
117
|
});
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Recreate a manager after server restart.
|
|
121
|
+
* Called by Syncable.tryRecreate() when client reconnects.
|
|
122
|
+
*
|
|
123
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
124
|
+
* @since 0.3.0
|
|
125
|
+
* @version 0.3.0
|
|
126
|
+
*
|
|
127
|
+
* @param {Conduit} conduit
|
|
128
|
+
* @param {Object} config Contains type, id, and version from the client
|
|
129
|
+
*
|
|
130
|
+
* @return {EditorToolbarManager}
|
|
131
|
+
*/
|
|
132
|
+
EditorToolbarManager.setStatic(async function recreate(conduit, config) {
|
|
133
|
+
|
|
134
|
+
// The config.id is the scene_id from the client.
|
|
135
|
+
// Set it on the conduit if not already set, so create() can use it.
|
|
136
|
+
if (!conduit.scene_id) {
|
|
137
|
+
conduit.scene_id = config.id;
|
|
138
|
+
} else if (conduit.scene_id !== config.id) {
|
|
139
|
+
// Scene ID mismatch - this shouldn't happen but log if it does
|
|
140
|
+
log.warning('EditorToolbarManager recreate: scene_id mismatch', {
|
|
141
|
+
conduit_scene_id : conduit.scene_id,
|
|
142
|
+
config_id : config.id,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return this.create(conduit);
|
|
147
|
+
});
|
|
64
148
|
}
|
|
65
149
|
|
|
66
150
|
/**
|
|
@@ -99,6 +183,24 @@ EditorToolbarManager.setStateProperty('title', {allow_client_set: false});
|
|
|
99
183
|
*/
|
|
100
184
|
EditorToolbarManager.setStateProperty('scenario');
|
|
101
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Whether this is a user-specific dashboard
|
|
188
|
+
*
|
|
189
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
190
|
+
* @since 0.3.0
|
|
191
|
+
* @version 0.3.0
|
|
192
|
+
*/
|
|
193
|
+
EditorToolbarManager.setStateProperty('is_user_dashboard', {allow_client_set: false});
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Whether the user has an ID (is logged in)
|
|
197
|
+
*
|
|
198
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
199
|
+
* @since 0.3.0
|
|
200
|
+
* @version 0.3.0
|
|
201
|
+
*/
|
|
202
|
+
EditorToolbarManager.setStateProperty('has_user_id', {allow_client_set: false});
|
|
203
|
+
|
|
102
204
|
/**
|
|
103
205
|
* Clear the model fallback
|
|
104
206
|
*
|
|
@@ -162,10 +264,11 @@ EditorToolbarManager.setTypedMethod([Types.String.optional().nullable()], functi
|
|
|
162
264
|
this.emitPropertyChange('model_name');
|
|
163
265
|
}
|
|
164
266
|
|
|
267
|
+
// Call registered model button providers
|
|
165
268
|
if (Blast.isNode && model_name) {
|
|
166
|
-
this.
|
|
167
|
-
|
|
168
|
-
}
|
|
269
|
+
for (let provider of this.constructor.model_button_providers) {
|
|
270
|
+
provider(this, model_name);
|
|
271
|
+
}
|
|
169
272
|
}
|
|
170
273
|
});
|
|
171
274
|
|
|
@@ -200,20 +303,9 @@ EditorToolbarManager.setMethod(function setDocument(doc) {
|
|
|
200
303
|
|
|
201
304
|
this.setDocumentWatcher(document_watcher);
|
|
202
305
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
record_pk: pk_val,
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (model.chimera.record_preview) {
|
|
211
|
-
if (this.scenario == 'chimera') {
|
|
212
|
-
this.addTemplateToRender('buttons', 'chimera/toolbar/preview_button', {
|
|
213
|
-
model_name: model_name.underscore(),
|
|
214
|
-
record_pk: pk_val,
|
|
215
|
-
});
|
|
216
|
-
}
|
|
306
|
+
// Call registered document button providers
|
|
307
|
+
for (let provider of this.constructor.document_button_providers) {
|
|
308
|
+
provider(this, doc, model, model_name, pk_val);
|
|
217
309
|
}
|
|
218
310
|
|
|
219
311
|
return document_watcher;
|
|
@@ -35,6 +35,245 @@ Widget.makeAbstractClass();
|
|
|
35
35
|
*/
|
|
36
36
|
Widget.startNewGroup('widgets');
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Standard widget categories.
|
|
40
|
+
* These are the universal categories available in all projects.
|
|
41
|
+
* Projects can register their own categories using Widget.registerCategory()
|
|
42
|
+
*
|
|
43
|
+
* Translation keys are derived automatically from the category name:
|
|
44
|
+
* - Title: category.name (e.g., "layout")
|
|
45
|
+
* - Description: "widget-category-{name}-description" (e.g., "widget-category-layout-description")
|
|
46
|
+
*
|
|
47
|
+
* Templates should pass filter parameters: widget=true category=true
|
|
48
|
+
*/
|
|
49
|
+
Widget.CATEGORIES = {
|
|
50
|
+
LAYOUT: {
|
|
51
|
+
name: 'layout',
|
|
52
|
+
icon: 'table-columns',
|
|
53
|
+
order: 10
|
|
54
|
+
},
|
|
55
|
+
TEXT: {
|
|
56
|
+
name: 'text',
|
|
57
|
+
icon: 'font',
|
|
58
|
+
order: 20
|
|
59
|
+
},
|
|
60
|
+
MEDIA: {
|
|
61
|
+
name: 'media',
|
|
62
|
+
icon: 'image',
|
|
63
|
+
order: 30
|
|
64
|
+
},
|
|
65
|
+
DATA: {
|
|
66
|
+
name: 'data',
|
|
67
|
+
icon: 'database',
|
|
68
|
+
order: 40
|
|
69
|
+
},
|
|
70
|
+
NAVIGATION: {
|
|
71
|
+
name: 'navigation',
|
|
72
|
+
icon: 'compass',
|
|
73
|
+
order: 50
|
|
74
|
+
},
|
|
75
|
+
INTERACTIVE: {
|
|
76
|
+
name: 'interactive',
|
|
77
|
+
icon: 'hand-pointer',
|
|
78
|
+
order: 60
|
|
79
|
+
},
|
|
80
|
+
ADVANCED: {
|
|
81
|
+
name: 'advanced',
|
|
82
|
+
icon: 'code',
|
|
83
|
+
order: 100
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Storage for dynamically registered categories.
|
|
89
|
+
* Use Widget.registerCategory() to add new categories.
|
|
90
|
+
*/
|
|
91
|
+
Widget.REGISTERED_CATEGORIES = {};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Register a new widget category.
|
|
95
|
+
* This allows projects to add their own categories without modifying the core.
|
|
96
|
+
*
|
|
97
|
+
* Translation keys are derived automatically from the category name:
|
|
98
|
+
* - Title: category.name (e.g., "monitoring")
|
|
99
|
+
* - Description: "widget-category-{name}-description" (e.g., "widget-category-monitoring-description")
|
|
100
|
+
*
|
|
101
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
102
|
+
* @since 0.3.0
|
|
103
|
+
* @version 0.3.0
|
|
104
|
+
*
|
|
105
|
+
* @param {string} key The category key (e.g., 'MONITORING')
|
|
106
|
+
* @param {Object} config Category configuration
|
|
107
|
+
* @param {string} config.name Internal name (e.g., 'monitoring')
|
|
108
|
+
* @param {string} config.icon FontAwesome icon name
|
|
109
|
+
* @param {number} [config.order] Sort order (lower = earlier, default: 90)
|
|
110
|
+
*/
|
|
111
|
+
Widget.setStatic(function registerCategory(key, config) {
|
|
112
|
+
|
|
113
|
+
if (!key || typeof key !== 'string') {
|
|
114
|
+
throw new Error('Category key must be a non-empty string');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!config || typeof config !== 'object') {
|
|
118
|
+
throw new Error('Category config must be an object');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!config.name || typeof config.name !== 'string') {
|
|
122
|
+
throw new Error('Category config.name must be a non-empty string');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!config.icon || typeof config.icon !== 'string') {
|
|
126
|
+
throw new Error('Category config.icon must be a non-empty string');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Normalize the key to uppercase
|
|
130
|
+
key = key.toUpperCase();
|
|
131
|
+
|
|
132
|
+
// Create the category config with defaults
|
|
133
|
+
let category = {
|
|
134
|
+
name : config.name,
|
|
135
|
+
icon : config.icon,
|
|
136
|
+
order : config.order ?? 90
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Store in registered categories
|
|
140
|
+
this.REGISTERED_CATEGORIES[key] = category;
|
|
141
|
+
|
|
142
|
+
// Also add to CATEGORIES for backwards compatibility and easy access
|
|
143
|
+
this.CATEGORIES[key] = category;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Set the widget category
|
|
148
|
+
*
|
|
149
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
150
|
+
* @since 0.3.0
|
|
151
|
+
* @version 0.3.0
|
|
152
|
+
*
|
|
153
|
+
* @param {String} category The category name (use CATEGORIES.*.name or custom string)
|
|
154
|
+
*/
|
|
155
|
+
Widget.setStatic(function setCategory(category) {
|
|
156
|
+
this.category = category;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Set the widget description
|
|
161
|
+
*
|
|
162
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
163
|
+
* @since 0.3.0
|
|
164
|
+
* @version 0.3.0
|
|
165
|
+
*
|
|
166
|
+
* @param {String} description A short description of what the widget does
|
|
167
|
+
*/
|
|
168
|
+
Widget.setStatic(function setDescription(description) {
|
|
169
|
+
this.description = description;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Set the widget icon (FontAwesome icon name)
|
|
174
|
+
*
|
|
175
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
176
|
+
* @since 0.3.0
|
|
177
|
+
* @version 0.3.0
|
|
178
|
+
*
|
|
179
|
+
* @param {String} icon The icon name (e.g., 'table', 'font', 'image')
|
|
180
|
+
*/
|
|
181
|
+
Widget.setStatic(function setIcon(icon) {
|
|
182
|
+
this.icon = icon;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Override the auto-generated title
|
|
187
|
+
*
|
|
188
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
189
|
+
* @since 0.3.0
|
|
190
|
+
* @version 0.3.0
|
|
191
|
+
*
|
|
192
|
+
* @param {String} title The display title for the widget
|
|
193
|
+
*/
|
|
194
|
+
Widget.setStatic(function setTitle(title) {
|
|
195
|
+
this.title = title;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get category info for a given category name.
|
|
200
|
+
* Supports built-in categories, registered categories, and custom strings.
|
|
201
|
+
* Falls back to ADVANCED category if no category is specified.
|
|
202
|
+
*
|
|
203
|
+
* Translation keys are derived from the category name at render time:
|
|
204
|
+
* - Title: category.name (e.g., "layout")
|
|
205
|
+
* - Description: "widget-category-{name}-description"
|
|
206
|
+
*
|
|
207
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
208
|
+
* @since 0.3.0
|
|
209
|
+
* @version 0.3.0
|
|
210
|
+
*
|
|
211
|
+
* @param {String} category The category name
|
|
212
|
+
*
|
|
213
|
+
* @return {Object} Category info with name, icon, order
|
|
214
|
+
*/
|
|
215
|
+
Widget.setStatic(function getCategoryInfo(category) {
|
|
216
|
+
|
|
217
|
+
// Default to 'advanced' if no category specified
|
|
218
|
+
if (!category) {
|
|
219
|
+
return this.CATEGORIES.ADVANCED;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Check built-in categories (includes registered ones added via registerCategory)
|
|
223
|
+
for (let key in this.CATEGORIES) {
|
|
224
|
+
if (this.CATEGORIES[key].name === category) {
|
|
225
|
+
return this.CATEGORIES[key];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Return a default structure for unknown/custom categories
|
|
230
|
+
// These are categories set via widget.category = 'custom' without registerCategory()
|
|
231
|
+
return {
|
|
232
|
+
name : category,
|
|
233
|
+
icon : 'puzzle-piece',
|
|
234
|
+
order : 90
|
|
235
|
+
};
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get all available categories.
|
|
240
|
+
* Returns built-in categories, registered categories, and any custom categories
|
|
241
|
+
* discovered from widgets that set their category to a custom string.
|
|
242
|
+
*
|
|
243
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
244
|
+
* @since 0.3.0
|
|
245
|
+
* @version 0.3.0
|
|
246
|
+
*
|
|
247
|
+
* @return {Array} Array of category info objects, sorted by order
|
|
248
|
+
*/
|
|
249
|
+
Widget.setStatic(function getAllCategories() {
|
|
250
|
+
|
|
251
|
+
let categories = new Map();
|
|
252
|
+
|
|
253
|
+
// Add built-in categories (includes registered ones added via registerCategory)
|
|
254
|
+
for (let key in this.CATEGORIES) {
|
|
255
|
+
let cat = this.CATEGORIES[key];
|
|
256
|
+
categories.set(cat.name, cat);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Check all registered widgets for custom categories
|
|
260
|
+
// (widgets that set category to a custom string without using registerCategory)
|
|
261
|
+
let widgets = alchemy.getClassGroup('widgets');
|
|
262
|
+
|
|
263
|
+
if (widgets) {
|
|
264
|
+
for (let widget of Object.values(widgets)) {
|
|
265
|
+
let category = widget.category;
|
|
266
|
+
|
|
267
|
+
if (category && !categories.has(category)) {
|
|
268
|
+
categories.set(category, this.getCategoryInfo(category));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Convert to array and sort by order
|
|
274
|
+
return Array.from(categories.values()).sortByPath(1, 'order');
|
|
275
|
+
});
|
|
276
|
+
|
|
38
277
|
/**
|
|
39
278
|
* Return the class-wide schema
|
|
40
279
|
*
|