alchemy-widget 0.3.0-alpha.1 → 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.
@@ -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
  *
@@ -740,7 +979,7 @@ Widget.setMethod(function populateWidget() {
740
979
  *
741
980
  * @author Jelle De Loecker <jelle@elevenways.be>
742
981
  * @since 0.1.0
743
- * @version 0.2.1
982
+ * @version 0.3.0
744
983
  */
745
984
  Widget.setMethod(function finalizePopulatedWidget() {
746
985
 
@@ -758,6 +997,22 @@ Widget.setMethod(function finalizePopulatedWidget() {
758
997
  }
759
998
  }
760
999
 
1000
+ if (config.main_class_names) {
1001
+ let main = this.widget?.children?.[0];
1002
+
1003
+ if (main) {
1004
+ let name,
1005
+ i;
1006
+
1007
+ let class_names = Array.cast(config.main_class_names);
1008
+
1009
+ for (i = 0; i < class_names.length; i++) {
1010
+ name = class_names[i];
1011
+ main.classList.add(name);
1012
+ }
1013
+ }
1014
+ }
1015
+
761
1016
  if (config.language) {
762
1017
  this.widget.setAttribute('lang', config.language);
763
1018
  } else {
@@ -10,3 +10,7 @@
10
10
  * @param {Object} data
11
11
  */
12
12
  const Column = Function.inherits('Alchemy.Widget.Container', 'Column');
13
+
14
+ // Widget metadata
15
+ Column.setCategory('layout');
16
+ Column.setIcon('grip-lines-vertical');
@@ -11,6 +11,27 @@
11
11
  */
12
12
  const List = Function.inherits('Alchemy.Widget.Container', 'List');
13
13
 
14
+ // Widget metadata
15
+ List.setCategory('layout');
16
+ List.setIcon('list');
17
+
18
+ /**
19
+ * Prepare the schema
20
+ *
21
+ * @author Jelle De Loecker <jelle@elevenways.be>
22
+ * @since 0.3.0
23
+ * @version 0.3.0
24
+ */
25
+ List.constitute(function prepareSchema() {
26
+
27
+ // Classnames for the li elements
28
+ this.schema.addField('li_class_names', 'String', {
29
+ title : 'Li-element CSS classes',
30
+ description : 'Configure extra CSS classes for the list items',
31
+ array: true,
32
+ });
33
+ });
34
+
14
35
  /**
15
36
  * Get a list of elements that could be child widgets
16
37
  *
@@ -39,4 +60,38 @@ List.setMethod(function initContainer() {
39
60
  this.widget.list_element = ul;
40
61
 
41
62
  initContainer.super.call(this);
63
+ });
64
+
65
+ /**
66
+ * Populate the contents of the widget
67
+ *
68
+ * @author Jelle De Loecker <jelle@elevenways.be>
69
+ * @since 0.3.0
70
+ * @version 0.3.0
71
+ */
72
+ List.setMethod(function finalizePopulatedWidget() {
73
+
74
+ const config = this.config;
75
+
76
+ if (config?.li_class_names) {
77
+ let li_elements = this.widget.list_element.querySelectorAll(':scope > li');
78
+
79
+ if (li_elements.length) {
80
+ let name,
81
+ i;
82
+
83
+ let class_names = Array.cast(config.li_class_names);
84
+
85
+ for (i = 0; i < class_names.length; i++) {
86
+ name = class_names[i];
87
+
88
+ for (let li_element of li_elements) {
89
+ li_element.classList.add(name);
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+
96
+ return finalizePopulatedWidget.super.call(this);
42
97
  });
@@ -10,3 +10,7 @@
10
10
  * @param {Object} data
11
11
  */
12
12
  const Row = Function.inherits('Alchemy.Widget.Container', 'Row');
13
+
14
+ // Widget metadata
15
+ Row.setCategory('layout');
16
+ Row.setIcon('grip-lines');
@@ -11,6 +11,11 @@
11
11
  */
12
12
  const AlchemyField = Function.inherits('Alchemy.Widget', 'AlchemyField');
13
13
 
14
+ // Widget metadata
15
+ AlchemyField.setTitle('Form Field');
16
+ AlchemyField.setCategory('data');
17
+ AlchemyField.setIcon('i-cursor');
18
+
14
19
  /**
15
20
  * Prepare the schema
16
21
  *
@@ -11,6 +11,11 @@
11
11
  */
12
12
  const AlchemyForm = Function.inherits('Alchemy.Widget', 'AlchemyForm');
13
13
 
14
+ // Widget metadata
15
+ AlchemyForm.setTitle('Form');
16
+ AlchemyForm.setCategory('data');
17
+ AlchemyForm.setIcon('rectangle-list');
18
+
14
19
  /**
15
20
  * Prepare the schema
16
21
  *
@@ -11,6 +11,10 @@
11
11
  */
12
12
  const AlchemyTable = Function.inherits('Alchemy.Widget', 'AlchemyTable');
13
13
 
14
+ // Widget metadata
15
+ AlchemyTable.setCategory('data');
16
+ AlchemyTable.setIcon('table');
17
+
14
18
  /**
15
19
  * Prepare the schema
16
20
  *
@@ -11,6 +11,11 @@
11
11
  */
12
12
  const Tabs = Function.inherits('Alchemy.Widget', 'AlchemyTabs');
13
13
 
14
+ // Widget metadata
15
+ Tabs.setTitle('Tabs');
16
+ Tabs.setCategory('layout');
17
+ Tabs.setIcon('folder');
18
+
14
19
  /**
15
20
  * Prepare the schema
16
21
  *
@@ -11,6 +11,10 @@
11
11
  */
12
12
  const Template = Function.inherits('Alchemy.Widget.Sourcecode', 'HawkejsTemplate');
13
13
 
14
+ // Widget metadata
15
+ Template.setCategory('advanced');
16
+ Template.setIcon('file-lines');
17
+
14
18
  /**
15
19
  * Populate the widget
16
20
  *
@@ -11,6 +11,10 @@
11
11
  */
12
12
  const Header = Function.inherits('Alchemy.Widget', 'Header');
13
13
 
14
+ // Widget metadata
15
+ Header.setCategory('text');
16
+ Header.setIcon('heading');
17
+
14
18
  /**
15
19
  * Prepare the schema
16
20
  *
@@ -12,6 +12,10 @@
12
12
  */
13
13
  const Html = Function.inherits('Alchemy.Widget', 'Html');
14
14
 
15
+ // Widget metadata
16
+ Html.setCategory('advanced');
17
+ Html.setIcon('code');
18
+
15
19
  /**
16
20
  * Prepare the schema
17
21
  *
@@ -11,6 +11,10 @@
11
11
  */
12
12
  const Markdown = Function.inherits('Alchemy.Widget', 'Markdown');
13
13
 
14
+ // Widget metadata
15
+ Markdown.setCategory('text');
16
+ Markdown.setIcon('file-code');
17
+
14
18
  /**
15
19
  * Prepare the schema
16
20
  *
@@ -11,6 +11,10 @@
11
11
  */
12
12
  const Sourcecode = Function.inherits('Alchemy.Widget', 'Sourcecode');
13
13
 
14
+ // Widget metadata
15
+ Sourcecode.setCategory('advanced');
16
+ Sourcecode.setIcon('file-code');
17
+
14
18
  /**
15
19
  * Prepare the schema
16
20
  *
@@ -11,6 +11,10 @@
11
11
  */
12
12
  const Toc = Function.inherits('Alchemy.Widget', 'TableOfContents');
13
13
 
14
+ // Widget metadata
15
+ Toc.setCategory('navigation');
16
+ Toc.setIcon('list-ol');
17
+
14
18
  /**
15
19
  * Populate the widget
16
20
  *
@@ -11,6 +11,10 @@
11
11
  */
12
12
  const Text = Function.inherits('Alchemy.Widget', 'Text');
13
13
 
14
+ // Widget metadata
15
+ Text.setCategory('text');
16
+ Text.setIcon('align-left');
17
+
14
18
  /**
15
19
  * Prepare the schema
16
20
  *
@@ -1,3 +1,5 @@
1
+ const MANAGER = Symbol('toolbar_manager');
2
+
1
3
  /**
2
4
  * Add a method to the conduit class to set the toolbar manager
3
5
  *
@@ -13,6 +15,8 @@ Classes.Alchemy.Conduit.Conduit.setMethod(function setToolbarInfo(document_or_mo
13
15
  } catch (err) {
14
16
  console.error('Error setting toolbar info', err);
15
17
  }
18
+
19
+ return this[MANAGER];
16
20
  });
17
21
 
18
22
  /**
@@ -30,11 +34,18 @@ function _setToolbarInfo(document_or_model, scenario = 'frontend') {
30
34
  return;
31
35
  }
32
36
 
33
- let manager = Classes.Alchemy.Widget.EditorToolbarManager.create(this);
34
- manager.scenario = scenario;
37
+ let manager = this[MANAGER] || this.set('toolbar_manager');
38
+
39
+ if (!manager) {
40
+ manager = Classes.Alchemy.Widget.EditorToolbarManager.create(this);
41
+ this[MANAGER] = manager;
42
+ this.set('toolbar_manager', manager);
43
+ this.expose('toolbar_manager', manager);
44
+ }
35
45
 
36
- this.set('toolbar_manager', manager);
37
- this.expose('toolbar_manager', manager);
46
+ if (scenario !== undefined) {
47
+ manager.scenario = scenario;
48
+ }
38
49
 
39
50
  let document_watcher,
40
51
  document,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "alchemy-widget",
3
3
  "description": "The widget plugin for the AlchemyMVC",
4
- "version": "0.3.0-alpha.1",
4
+ "version": "0.3.0",
5
5
  "author": "Jelle De Loecker <jelle@elevenways.be>",
6
6
  "keywords": [
7
7
  "alchemy",
@@ -11,7 +11,7 @@
11
11
  ],
12
12
  "peerDependencies": {
13
13
  "alchemymvc" : ">=1.4.0||>=1.4.0-alpha",
14
- "alchemy-form": "~0.3.0||~0.3.0-alpha"
14
+ "alchemy-form": ">=0.3.0||>=0.3.0-alpha"
15
15
  },
16
16
  "repository": "11ways/alchemy-widget",
17
17
  "license": "MIT",
@@ -1,3 +1,51 @@
1
1
  <div data-he-slot="left"></div>
2
+
2
3
  <div data-he-slot="center"></div>
3
- <div data-he-slot="right"></div>
4
+
5
+ <div data-he-slot="right">
6
+ <al-button class="start-edit" state="ready">
7
+ <al-icon icon-name="pencil"></al-icon>
8
+ {%t "start-editing" %}
9
+ </al-button>
10
+
11
+ <al-button class="stop-and-save" state="ready">
12
+ <al-state state-name="saving">
13
+ <al-icon icon-name="spinner" icon-flags="spin"></al-icon>
14
+ {%t "saving" %}
15
+ </al-state>
16
+ <al-state state-name="saving-before-stop">
17
+ <al-icon icon-name="spinner" icon-flags="spin"></al-icon>
18
+ {%t "saving" %}
19
+ </al-state>
20
+ <al-state state-name="saved">
21
+ <al-icon icon-name="badge-check" icon-flags="beat"></al-icon>
22
+ {%t "saved" %}
23
+ </al-state>
24
+ <al-state state-name="ready">
25
+ <al-icon icon-name="floppy-disk-circle-arrow-right"></al-icon>
26
+ {%t "save-and-stop-editing" %}
27
+ </al-state>
28
+ </al-button>
29
+
30
+ <al-button class="stop-edit" state="ready">
31
+ <al-icon icon-name="pencil-slash"></al-icon>
32
+ {%t "stop-editing" %}
33
+ </al-button>
34
+
35
+ <al-button class="save-all" state="ready">
36
+ <al-state state-name="saving">
37
+ <al-icon icon-name="spinner" icon-flags="spin"></al-icon>
38
+ {%t "saving" %}
39
+ </al-state>
40
+ <al-state state-name="saved">
41
+ <al-icon icon-name="badge-check" icon-flags="beat"></al-icon>
42
+ {%t "saved" %}
43
+ </al-state>
44
+ <al-state state-name="ready">
45
+ <al-icon icon-name="floppy-disk"></al-icon>
46
+ {%t "save-all" %}
47
+ </al-state>
48
+ </al-button>
49
+
50
+ <div data-area="buttons"></div>
51
+ </div>
@@ -41,7 +41,7 @@
41
41
  </al-state>
42
42
  </al-button>
43
43
 
44
- {% if Router.routeConfig('Chimera.Editor#index') %}
44
+ {% if Router.routeConfig('Chimera.Editor#index') && (not toolbar_manager or toolbar_manager.scenario neq 'chimera') %}
45
45
  <a href="/chimera/" data-he-link="false">
46
46
  <al-icon icon-style="duotone" icon-name="display-code"></al-icon>
47
47
  {%t "go-to-backend" %}
@@ -0,0 +1,55 @@
1
+ {# Widget picker dialog - using flat widgets list to avoid nested loop issues #}
2
+ <div class="widget-picker">
3
+ <div class="widget-picker-header">
4
+ <input
5
+ type="search"
6
+ class="widget-picker-search"
7
+ placeholder={% __d('widget-picker-search-placeholder') %}
8
+ autocomplete="off"
9
+ >
10
+ </div>
11
+
12
+ <div class="widget-picker-body">
13
+ <aside class="widget-picker-categories">
14
+ <button class="category-btn active" data-category="">
15
+ <al-icon icon-name="grid-2"></al-icon>
16
+ <span>{%t 'widget-picker-all-categories' %}</span>
17
+ </button>
18
+ {% each categories as category %}
19
+ <button
20
+ class="category-btn"
21
+ data-category={% category.name %}
22
+ >
23
+ <al-icon icon-name={% category.icon %}></al-icon>
24
+ <span>{%t category.name widget=true category=true %}</span>
25
+ </button>
26
+ {% /each %}
27
+ </aside>
28
+
29
+ <main class="widget-picker-list">
30
+ {% each widgets as widget %}
31
+ <button class="widget-item"
32
+ data-type={% widget.type_name %}
33
+ data-category={% widget.category %}
34
+ tabindex="0">
35
+ <div class="widget-item-icon">
36
+ <al-icon icon-name={% widget.icon %}></al-icon>
37
+ </div>
38
+ <div class="widget-item-content">
39
+ <span class="widget-item-title">{{ widget.title }}</span>
40
+ {% if widget.description %}
41
+ <span class="widget-item-description">{{ widget.description }}</span>
42
+ {% else %}
43
+ <span class="widget-item-description">{%t widget.description_key widget=true %}</span>
44
+ {% /if %}
45
+ </div>
46
+ </button>
47
+ {% /each %}
48
+
49
+ <div class="widget-picker-empty" hidden>
50
+ <al-icon icon-name="search"></al-icon>
51
+ <p>{%t 'widget-picker-no-results' %}</p>
52
+ </div>
53
+ </main>
54
+ </div>
55
+ </div>