alchemy-widget 0.1.4 → 0.1.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/CHANGELOG.md CHANGED
@@ -1,3 +1,28 @@
1
+ ## 0.1.6 (2022-10-12)
2
+
3
+ * Allow hiding widgets from the add-menu
4
+ * Let actions return their button contents as elements instead of only html
5
+ * Cancel clicks on widgets when editing them
6
+ * Fix getting the `hawkejs_renderer` instance in a widget
7
+ * Use `alchemy-chimera` style for the widget configuration dialog
8
+ * Make renders wait for widgets that have to render their content asynchronously
9
+ * Allow setting the element to use in a Text widget
10
+ * Fix Header-widget level actions
11
+ * Load the icon fonts as soon as the editor starts
12
+ * Make the `rerender` method async
13
+ * Use `child_class` property in the populate method
14
+ * Add filter logic to widgets for getting specific values
15
+ * Add abstract `Partial` widget class, to easily create a new widget with a pre-defined layout
16
+ * Wait for widgets to render their contents before starting editor
17
+ * Add `can_be_removed` property to widget elements
18
+ * Add `can_be_moved` property to widget elements
19
+ * Throw an error if `alchemy-form` is loaded before this plugin
20
+
21
+ ## 0.1.5 (2022-07-14)
22
+
23
+ * Unselect widgets when stopping the editor
24
+ * Add front-end save ability to widgets
25
+
1
26
  ## 0.1.4 (2022-06-23)
2
27
 
3
28
  * Use `he-context-menu` element to show widgets to add
@@ -1,5 +1,12 @@
1
1
  @import "alchemy-widget-symbols.scss";
2
2
 
3
+ alchemy-widgets,
4
+ alchemy-widgets-row,
5
+ alchemy-widgets-column,
6
+ alchemy-widget {
7
+
8
+ }
9
+
3
10
  alchemy-widgets,
4
11
  alchemy-widgets-row,
5
12
  alchemy-widgets-column {
@@ -11,7 +18,14 @@ alchemy-widgets-column {
11
18
  }
12
19
 
13
20
  > * {
14
- flex: 10;
21
+ flex: 10 10 auto;
22
+ }
23
+ }
24
+
25
+ alchemy-widget {
26
+ &.aw-editing {
27
+ outline: 2px dashed rgba(0, 0, 0, 0.3);
28
+ outline-offset: -2px;
15
29
  }
16
30
  }
17
31
 
@@ -35,7 +49,7 @@ alchemy-widgets-column {
35
49
 
36
50
  > alchemy-widget-add-area {
37
51
  position: absolute;
38
- bottom: 1.5rem;
52
+ bottom: 0.2rem;
39
53
  left: 50%;
40
54
  transform: translateX(-50%);
41
55
  }
@@ -67,6 +81,18 @@ alchemy-widgets-row {
67
81
  }
68
82
  }
69
83
 
84
+ alchemy-widget-add-area {
85
+ background: rgba(255,255,255,0.5);
86
+ padding: 0.5rem 2rem;
87
+ border-radius: 2rem;
88
+ z-index: 99999999;
89
+
90
+ .widget-button {
91
+ min-height: 2.5rem;
92
+ padding: 1rem;
93
+ }
94
+ }
95
+
70
96
  alchemy-widgets-row,
71
97
  alchemy-widgets > alchemy-widgets-column,
72
98
  alchemy-widgets-column > alchemy-widgets-column,
@@ -164,7 +190,10 @@ alchemy-widget {
164
190
  min-height: 2rem;
165
191
 
166
192
  &:hover {
167
- background: rgba(60, 60, 120, 0.1);
193
+ background: rgba(60, 60, 120, 0.2);
194
+
195
+ // This actually causes some glitches on Firefox :/
196
+ backdrop-filter: blur(4px);
168
197
  }
169
198
  }
170
199
 
@@ -181,6 +210,7 @@ alchemy-widget-toolbar {
181
210
  background-color: white;
182
211
  border-radius: 4px;
183
212
  border: 1px solid #dadada;
213
+ font-size: 2rem;
184
214
 
185
215
  &[hidden] {
186
216
  display: none;
@@ -200,8 +230,8 @@ alchemy-widget-context {
200
230
  }
201
231
 
202
232
  alchemy-widget-toolbar {
203
- min-height: 2rem;
204
- min-width: 2rem;
233
+ min-height: 2.5rem;
234
+ min-width: 2.5rem;
205
235
  display: flex;
206
236
 
207
237
  > * {
@@ -209,7 +239,9 @@ alchemy-widget-toolbar {
209
239
  }
210
240
 
211
241
  .aw-toolbar-button {
242
+ font-size: 2.5rem;
212
243
  border-radius: 4px;
244
+ padding: 0.6rem;
213
245
 
214
246
  &:hover {
215
247
  color: rgb(112, 118, 132);
@@ -240,6 +272,22 @@ alchemy-widget[type="header"] {
240
272
  }
241
273
  }
242
274
 
275
+ alchemy-widget[type="text"] {
276
+
277
+ &.aw-editing {
278
+ &,
279
+ & > * {
280
+ min-width: 5rem;
281
+ min-height: 3rem;
282
+ }
283
+
284
+ & > * {
285
+ display: inline-block;
286
+ width: 100%;
287
+ }
288
+ }
289
+ }
290
+
243
291
  .aw-toolbar-button {
244
292
 
245
293
  .aw-header-h {
@@ -255,4 +303,14 @@ alchemy-widget[type="header"] {
255
303
 
256
304
  table-of-contents {
257
305
  display: block;
306
+ }
307
+
308
+ [data-he-template="widget/widget_config"] {
309
+ alchemy-label {
310
+ padding: 0.5rem;
311
+
312
+ [data-he-name="field-title"] {
313
+ display: block;
314
+ }
315
+ }
258
316
  }
package/bootstrap.js ADDED
@@ -0,0 +1,10 @@
1
+ if (alchemy.plugins.form) {
2
+ throw new Error('The alchemy-form plugin has to be loaded AFTER alchemy-widget');
3
+ }
4
+
5
+ Router.add({
6
+ name : 'AlchemyWidgets#save',
7
+ methods : 'post',
8
+ paths : '/api/alchemywidgets/save',
9
+ policy : 'logged_in',
10
+ });
@@ -0,0 +1,187 @@
1
+ /**
2
+ * The Alchemy Widgets Controller class
3
+ *
4
+ * @author Jelle De Loecker <jelle@elevenways.be>
5
+ * @since 0.1.5
6
+ * @version 0.1.5
7
+ */
8
+ const AlchemyWidgets = Function.inherits('Alchemy.Controller', 'AlchemyWidgets');
9
+
10
+ /**
11
+ * Aggregate all the records to save
12
+ *
13
+ * @author Jelle De Loecker <jelle@elevenways.be>
14
+ * @since 0.1.5
15
+ * @version 0.1.6
16
+ *
17
+ * @param {Object[]} fields
18
+ *
19
+ * @return {Document[]}
20
+ */
21
+ AlchemyWidgets.setMethod(async function aggregate(widgets) {
22
+
23
+ let result = {};
24
+
25
+ for (let widget of widgets) {
26
+
27
+ if (!widget || !widget.model || !widget.field) {
28
+ throw new Error('Unable to save Widget: no model or field was given');
29
+ }
30
+
31
+ const model = alchemy.getModel(widget.model);
32
+
33
+ if (!model) {
34
+ throw new Error('Unable to save Widget: model "' + widget.model + '" not found');
35
+ }
36
+
37
+ let field = model.getField(widget.field);
38
+
39
+ if (!field) {
40
+ throw new Error('Unable to save Widget: field "' + widget.field + '" does not exist inside model "' + widget.model + '"');
41
+ }
42
+
43
+ let record;
44
+
45
+ if (widget.pk) {
46
+
47
+ if (result[widget.pk]) {
48
+ record = result[widget.pk];
49
+ } else {
50
+ record = await model.findByPk(widget.pk);
51
+ result[widget.pk] = record;
52
+ }
53
+ } else {
54
+
55
+ if (result[model.name]) {
56
+ record = result[model.name];
57
+ } else {
58
+ record = model.createDocument();
59
+ result[model.name] = record;
60
+ }
61
+ }
62
+
63
+ let field_definition;
64
+
65
+ if (widget.value_path) {
66
+ field_definition = model.getField(widget.field + '.' + widget.value_path);
67
+ } else {
68
+ field_definition = model.getField(widget.field);
69
+ }
70
+
71
+ if (!field_definition) {
72
+ continue;
73
+ }
74
+
75
+ // The optional translation key
76
+ let field_language = widget.field_languages?.[widget.field];
77
+ let target_field_value = record[widget.field];
78
+ let target_container;
79
+ let target_key;
80
+ let path_for_language;
81
+
82
+ // Do incredibly complicated filter stuff
83
+ if (widget.filter_value) {
84
+
85
+ if (!widget.filter_target || !widget.value_path) {
86
+ continue;
87
+ }
88
+
89
+ // Create the root field if needed
90
+ if (!target_field_value) {
91
+ target_field_value = [];
92
+ record[widget.field] = target_field_value;
93
+ }
94
+
95
+ if (target_field_value && Array.isArray(target_field_value)) {
96
+ target_key = widget.value_path;
97
+
98
+ for (let index = 0; index < target_field_value.length; index++) {
99
+ let entry = target_field_value[index];
100
+
101
+ if (entry[widget.filter_target] == widget.filter_value) {
102
+ target_container = entry;
103
+ path_for_language = widget.field + '.' + index + '.' + target_key;
104
+ break;
105
+ }
106
+ }
107
+
108
+ if (!target_container) {
109
+ target_container = {
110
+ [widget.filter_target] : widget.filter_value,
111
+ [target_key] : {},
112
+ };
113
+
114
+ let new_index = target_field_value.push(target_container) - 1;
115
+ path_for_language = widget.field + '.' + new_index + '.' + target_key;
116
+ }
117
+ }
118
+ } else {
119
+ target_container = record;
120
+ target_key = widget.field;
121
+ }
122
+
123
+ if (!field_language && path_for_language) {
124
+ field_language = widget.field_languages?.[path_for_language];
125
+ }
126
+
127
+ if (!field_language && field_definition.is_translatable) {
128
+ field_language = this.conduit.active_prefix;
129
+
130
+ if (!field_language) {
131
+ continue;
132
+ }
133
+ }
134
+
135
+ if (field_language) {
136
+ if (!target_container) {
137
+ target_container = record[widget.field] = {};
138
+ }
139
+
140
+
141
+ if (!target_container[target_key]) {
142
+ target_container[target_key] = {};
143
+ }
144
+
145
+ target_container = target_container[target_key];
146
+ target_key = field_language;
147
+ }
148
+
149
+ target_container[target_key] = widget.value;
150
+ }
151
+
152
+ return Object.values(result);
153
+ });
154
+
155
+ /**
156
+ * The save action
157
+ *
158
+ * @author Jelle De Loecker <jelle@elevenways.be>
159
+ * @since 0.1.5
160
+ * @version 0.1.5
161
+ *
162
+ * @param {Conduit} conduit
163
+ */
164
+ AlchemyWidgets.setAction(async function save(conduit) {
165
+
166
+ const body = conduit.body;
167
+
168
+ if (!body || !body.widgets?.length) {
169
+ return conduit.error('Unable to save Widgets: no widgets were given');
170
+ }
171
+
172
+ let records = await this.aggregate(body.widgets);
173
+
174
+ let saved_pks = [];
175
+
176
+ for (let record of records) {
177
+ await record.save();
178
+ saved_pks.push(record.$pk);
179
+ }
180
+
181
+ let result = {
182
+ saved_pks,
183
+ saved : true,
184
+ };
185
+
186
+ conduit.end(result);
187
+ });
@@ -83,6 +83,77 @@ Base.setProperty(function previous_container() {
83
83
  return this.getSiblingContainer('previous');
84
84
  });
85
85
 
86
+ /**
87
+ * Is this the root element?
88
+ *
89
+ * @author Jelle De Loecker <jelle@elevenways.be>
90
+ * @since 0.1.5
91
+ * @version 0.1.5
92
+ */
93
+ Base.setProperty(function is_root_widget() {
94
+ return !this.parent_container;
95
+ });
96
+
97
+ /**
98
+ * Can this widget be saved?
99
+ *
100
+ * @author Jelle De Loecker <jelle@elevenways.be>
101
+ * @since 0.1.5
102
+ * @version 0.1.5
103
+ */
104
+ Base.setProperty(function can_be_saved() {
105
+
106
+ if (!this.is_root_widget) {
107
+ return false;
108
+ }
109
+
110
+ if (!this.record || !this.field) {
111
+ return false;
112
+ }
113
+
114
+ return true;
115
+ });
116
+
117
+ /**
118
+ * Can this widget be removed?
119
+ *
120
+ * @author Jelle De Loecker <jelle@elevenways.be>
121
+ * @since 0.1.6
122
+ * @version 0.1.6
123
+ *
124
+ * @type {Boolean}
125
+ */
126
+ Base.setProperty(function can_be_removed() {
127
+
128
+ // Widgets without a "parent_instance" are mostly
129
+ // hardcoded in some template (like in a Partial widget)
130
+ if (!this.instance?.parent_instance) {
131
+ return false;
132
+ }
133
+
134
+ return true;
135
+ });
136
+
137
+ /**
138
+ * Can this widget be moved?
139
+ *
140
+ * @author Jelle De Loecker <jelle@elevenways.be>
141
+ * @since 0.1.6
142
+ * @version 0.1.6
143
+ *
144
+ * @type {Boolean}
145
+ */
146
+ Base.setProperty(function can_be_moved() {
147
+
148
+ // Widgets without a "parent_instance" are mostly
149
+ // hardcoded in some template (like in a Partial widget)
150
+ if (!this.instance?.parent_instance) {
151
+ return false;
152
+ }
153
+
154
+ return true;
155
+ });
156
+
86
157
  /**
87
158
  * Get a sibling container
88
159
  *
@@ -201,7 +272,7 @@ Base.setMethod(function rerender() {
201
272
  *
202
273
  * @author Jelle De Loecker <jelle@elevenways.be>
203
274
  * @since 0.1.0
204
- * @version 0.1.0
275
+ * @version 0.1.6
205
276
  */
206
277
  Base.setMethod(function introduced() {
207
278
 
@@ -209,4 +280,72 @@ Base.setMethod(function introduced() {
209
280
  this.startEditor();
210
281
  }
211
282
 
283
+ this.addEventListener('click', e => {
284
+
285
+ let is_editing = this.instance?.editing;
286
+
287
+ if (is_editing) {
288
+ let anchor = e.target.closest('a');
289
+
290
+ if (anchor) {
291
+ e.preventDefault();
292
+ }
293
+ }
294
+ });
295
+ });
296
+
297
+ /**
298
+ * Get the configuration used to save data
299
+ *
300
+ * @author Jelle De Loecker <jelle@elevenways.be>
301
+ * @since 0.1.5
302
+ * @version 0.1.6
303
+ */
304
+ Base.setMethod(function gatherSaveData() {
305
+
306
+ if (!this.is_root_widget) {
307
+ return;
308
+ }
309
+
310
+ if (!this.record || !this.field) {
311
+ return;
312
+ }
313
+
314
+ let result = {
315
+ model : this.record.$model_name,
316
+ pk : this.record.$pk,
317
+ field : this.field,
318
+ value : this.value,
319
+ filter_target : this.filter_target,
320
+ filter_value : this.filter_value,
321
+ value_path : this.value_path,
322
+ field_languages : this.record.$hold.translated_fields,
323
+ };
324
+
325
+ return result;
326
+ });
327
+
328
+ /**
329
+ * Save the current configuration
330
+ *
331
+ * @author Jelle De Loecker <jelle@elevenways.be>
332
+ * @since 0.1.5
333
+ * @version 0.1.5
334
+ */
335
+ Base.setMethod(async function save() {
336
+
337
+ let data = this.gatherSaveData();
338
+
339
+ if (!data) {
340
+ return;
341
+ }
342
+
343
+ let config = {
344
+ href : alchemy.routeUrl('AlchemyWidgets#save'),
345
+ post : {
346
+ widgets: [data]
347
+ }
348
+ };
349
+
350
+ let result = await alchemy.fetch(config);
212
351
  });
@@ -34,6 +34,60 @@ Widget.setAttribute('type');
34
34
  */
35
35
  Widget.setProperty('is_alchemy_widget', true);
36
36
 
37
+ /**
38
+ * The database record to work with
39
+ *
40
+ * @author Jelle De Loecker <jelle@elevenways.be>
41
+ * @since 0.1.5
42
+ * @version 0.1.5
43
+ */
44
+ Widget.setAssignedProperty('record');
45
+
46
+ /**
47
+ * The fieldname in the record to work with
48
+ *
49
+ * @author Jelle De Loecker <jelle@elevenways.be>
50
+ * @since 0.1.5
51
+ * @version 0.1.5
52
+ */
53
+ Widget.setAssignedProperty('field');
54
+
55
+ /**
56
+ * The path to the value inside the field
57
+ *
58
+ * @author Jelle De Loecker <jelle@elevenways.be>
59
+ * @since 0.1.6
60
+ * @version 0.1.6
61
+ */
62
+ Widget.setAssignedProperty('value_path');
63
+
64
+ /**
65
+ * The path to the field to filter on
66
+ *
67
+ * @author Jelle De Loecker <jelle@elevenways.be>
68
+ * @since 0.1.6
69
+ * @version 0.1.6
70
+ */
71
+ Widget.setAssignedProperty('filter_target');
72
+
73
+ /**
74
+ * The filter value
75
+ *
76
+ * @author Jelle De Loecker <jelle@elevenways.be>
77
+ * @since 0.1.6
78
+ * @version 0.1.6
79
+ */
80
+ Widget.setAssignedProperty('filter_value');
81
+
82
+ /**
83
+ * CSS classes to put on the direct children
84
+ *
85
+ * @author Jelle De Loecker <jelle@elevenways.be>
86
+ * @since 0.1.6
87
+ * @version 0.1.6
88
+ */
89
+ Widget.setAttribute('child-class');
90
+
37
91
  /**
38
92
  * Is this widget being edited?
39
93
  *
@@ -55,7 +109,7 @@ Widget.setProperty(function editing() {
55
109
  *
56
110
  * @author Jelle De Loecker <jelle@elevenways.be>
57
111
  * @since 0.1.0
58
- * @version 0.1.0
112
+ * @version 0.1.5
59
113
  */
60
114
  Widget.setProperty(function value() {
61
115
  return {
@@ -63,6 +117,107 @@ Widget.setProperty(function value() {
63
117
  config : this.instance.syncConfig(),
64
118
  }
65
119
  }, function setValue(value) {
120
+ this.applyValue(value);
121
+ });
122
+
123
+ /**
124
+ * Received a new record
125
+ *
126
+ * @author Jelle De Loecker <jelle@elevenways.be>
127
+ * @since 0.1.5
128
+ * @version 0.1.6
129
+ */
130
+ Widget.setMethod(function onRecordAssignment(new_record, old_val) {
131
+ if (new_record && this.field) {
132
+ let value = this.getValueFromRecord(new_record);
133
+ this.applyValue(value);
134
+ }
135
+ });
136
+
137
+ /**
138
+ * Received a new field name
139
+ *
140
+ * @author Jelle De Loecker <jelle@elevenways.be>
141
+ * @since 0.1.5
142
+ * @version 0.1.6
143
+ */
144
+ Widget.setMethod(function onFieldAssignment(new_field, old_val) {
145
+ if (new_field && this.record) {
146
+ let value = this.getValueFromRecord(this.record);
147
+ this.applyValue(value);
148
+ }
149
+ });
150
+
151
+ /**
152
+ * Get the values to work with from the given record
153
+ *
154
+ * @author Jelle De Loecker <jelle@elevenways.be>
155
+ * @since 0.1.6
156
+ * @version 0.1.6
157
+ */
158
+ Widget.setMethod(function getValueFromRecord(record) {
159
+
160
+ if (!record) {
161
+ return;
162
+ }
163
+
164
+ let value;
165
+
166
+ if (this.field) {
167
+ value = record[this.field];
168
+ } else {
169
+ value = record;
170
+ }
171
+
172
+ if (!value) {
173
+ return;
174
+ }
175
+
176
+ if (this.filter_value) {
177
+ if (!Array.isArray(value) || !this.filter_target) {
178
+ return;
179
+ }
180
+
181
+ let inputs = value;
182
+ value = [];
183
+
184
+ for (let input of inputs) {
185
+ if (input[this.filter_target] == this.filter_value) {
186
+ value.push(input);
187
+ }
188
+ }
189
+ }
190
+
191
+ if (this.value_path) {
192
+ if (!Array.isArray(value)) {
193
+ return;
194
+ }
195
+
196
+ let inputs = value;
197
+ value = [];
198
+
199
+ for (let input of inputs) {
200
+ if (input[this.value_path]) {
201
+ value.push(input[this.value_path]);
202
+ }
203
+ }
204
+ }
205
+
206
+ if (Array.isArray(value)) {
207
+ value = value[0];
208
+ }
209
+
210
+ return value;
211
+ });
212
+
213
+ /**
214
+ * Apply the given value
215
+ *
216
+ * @author Jelle De Loecker <jelle@elevenways.be>
217
+ * @since 0.1.5
218
+ * @version 0.1.6
219
+ */
220
+ Widget.setMethod(function applyValue(value) {
66
221
 
67
222
  let config,
68
223
  type;
@@ -100,7 +255,11 @@ Widget.setProperty(function value() {
100
255
  }
101
256
 
102
257
  this.instance.config = config;
103
- this.instance.populateWidget();
258
+ let promise = this.instance.populateWidget();
259
+
260
+ if (promise) {
261
+ this.delayAssemble(promise);
262
+ }
104
263
  });
105
264
 
106
265
  /**
@@ -136,7 +295,7 @@ Widget.setMethod(function startEditor() {
136
295
  *
137
296
  * @author Jelle De Loecker <jelle@elevenways.be>
138
297
  * @since 0.1.0
139
- * @version 0.1.0
298
+ * @version 0.1.5
140
299
  */
141
300
  Widget.setMethod(function stopEditor() {
142
301
 
@@ -144,6 +303,7 @@ Widget.setMethod(function stopEditor() {
144
303
  throw new Error('Unable to stop the editor: this widget element has no accompanying instance');
145
304
  }
146
305
 
306
+ this.unselectWidget();
147
307
  this.instance.stopEditor();
148
308
  this.removeEditEventListeners();
149
309
  });