alchemy-widget 0.2.0 → 0.2.2

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,27 @@
1
+ ## 0.2.2 (2022-12-23)
2
+
3
+ * Fix widgets not being populated properly
4
+
5
+ ## 0.2.1 (2022-12-23)
6
+
7
+ * Add a `-first` modifier class to the first visible entry of a table-of-contents element
8
+ * Make HTML widget editable
9
+ * Also store and retrieve copied widget config from localStorage
10
+ * Move `Container` widget init logic from the custom-element to the main widget instance
11
+ * Disable most `backdrop-filter` properties, they caused too many FPS issues
12
+ * Add the ability to hide widgets from the public
13
+ * Allow overriding the language (`lang` attribute) of a widget
14
+ * Fix config of container widgets not being saved
15
+ * Only add minium dimensions when editing
16
+ * Add `child-role` attribute & use it to set the default role of child widgets
17
+ * Add the `toc-has-content` or `toc-is-empty` class to `al-toc` elements
18
+ * Fix issue where `Text` widget would break when edited with LanguageTool extension
19
+ * Do not truncate titles in table-of-contents automatically
20
+ * Fix nesting levels in `al-toc`
21
+ * Only allow text inside header widgets
22
+ * Move default `Widget#populateWidget()` code to `Widget#finalizePopulatedWidget()`
23
+ * Add `HawkejsTemplate` widget
24
+
1
25
  ## 0.2.0 (2022-11-02)
2
26
 
3
27
  * Use `al-` prefix for all custom elements
@@ -2,6 +2,10 @@
2
2
  al-widget-toolbar {
3
3
  display: none;
4
4
  }
5
+
6
+ [hidden] {
7
+ display: none !important;
8
+ }
5
9
  }
6
10
 
7
11
  html.logged-in {
@@ -110,6 +114,7 @@ al-widgets-column {
110
114
  &.aw-editing {
111
115
  position: relative;
112
116
  min-height: 3rem;
117
+ min-width: 10rem;
113
118
  }
114
119
 
115
120
  > * {
@@ -127,7 +132,7 @@ al-widget {
127
132
  background: white;
128
133
  border: 2px dashed rgba(0, 0, 0, 0.4);
129
134
  pointer-events: none;
130
- backdrop-filter: invert(80%);
135
+ //backdrop-filter: invert(80%);
131
136
  clip-path: polygon(0% 0%, 0% 100%, 2px 100%, 2px 2px, calc(100% - 2px) 2px, calc(100% - 2px) calc(100% - 2px), 0 calc(100% - 2px), 0 100%, 100% 100%, 100% 0)
132
137
  }
133
138
  }
@@ -159,11 +164,6 @@ al-widgets-column {
159
164
  }
160
165
  }
161
166
 
162
- al-widgets {
163
- min-width: 10rem;
164
- min-height: 10rem;
165
- }
166
-
167
167
  al-widgets-row {
168
168
  flex-flow: row;
169
169
  flex: 10 10 auto;
@@ -287,12 +287,13 @@ al-widget {
287
287
 
288
288
  &.aw-editing {
289
289
  min-height: 2rem;
290
+ min-width: 2rem;
290
291
 
291
292
  &:hover {
292
293
  background: rgba(60, 60, 120, 0.2);
293
294
 
294
- // This actually causes some glitches on Firefox :/
295
- backdrop-filter: blur(4px);
295
+ // This actually causes some glitches on Firefox and chrome too :/
296
+ //backdrop-filter: blur(4px);
296
297
  }
297
298
  }
298
299
 
@@ -423,4 +424,8 @@ al-toc {
423
424
  display: block;
424
425
  }
425
426
  }
427
+ }
428
+
429
+ .aw-hidden {
430
+ opacity: 0.8;
426
431
  }
package/bootstrap.js CHANGED
@@ -4,9 +4,106 @@ if (!alchemy.plugins.form) {
4
4
  throw new Error('The alchemy-form plugin has to be loaded BEFORE alchemy-widget');
5
5
  }
6
6
 
7
+ /* Ckeditor 5 available toolbar buttons (from styleboost build)
8
+ [
9
+ "blockQuote",
10
+ "bold",
11
+ "code",
12
+ "codeBlock",
13
+ "selectAll",
14
+ "undo",
15
+ "redo",
16
+ "heading",
17
+ "horizontalLine",
18
+ "imageTextAlternative",
19
+ "toggleImageCaption",
20
+ "imageStyle:inline",
21
+ "imageStyle:alignLeft",
22
+ "imageStyle:alignRight",
23
+ "imageStyle:alignCenter",
24
+ "imageStyle:alignBlockLeft",
25
+ "imageStyle:alignBlockRight",
26
+ "imageStyle:block",
27
+ "imageStyle:side",
28
+ "imageStyle:wrapText",
29
+ "imageStyle:breakText",
30
+ "uploadImage",
31
+ "imageUpload",
32
+ "indent",
33
+ "outdent",
34
+ "italic",
35
+ "link",
36
+ "linkImage",
37
+ "numberedList",
38
+ "bulletedList",
39
+ "mediaEmbed",
40
+ "removeFormat",
41
+ "sourceEditing",
42
+ "strikethrough",
43
+ "insertTable",
44
+ "tableColumn",
45
+ "tableRow",
46
+ "mergeTableCells",
47
+ "toggleTableCaption",
48
+ "tableCellProperties",
49
+ "tableProperties",
50
+ "todoList"
51
+ ]
52
+ */
53
+
54
+ let options = {
55
+ ckeditor_path: null,
56
+ ckeditor_toolbar: [
57
+ 'heading',
58
+ '|',
59
+ 'bold', 'italic', 'link', 'bulletedList', 'numberedList',
60
+ '|',
61
+ 'indent',
62
+ 'outdent',
63
+ 'horizontalLine',
64
+ '|',
65
+ 'blockQuote',
66
+ 'code',
67
+ 'codeBlock',
68
+ '|',
69
+ 'imageUpload',
70
+ 'insertTable',
71
+ '|',
72
+ 'undo', 'redo',
73
+ ],
74
+ };
75
+
76
+ // Inject the user-overridden options
77
+ alchemy.plugins.widget = Object.assign(options, alchemy.plugins.widget);
78
+
79
+ if (!options.ckeditor_path) {
80
+ if (alchemy.plugins.styleboost) {
81
+ options.ckeditor_path = '/public/ckeditor/5/ckeditor.js';
82
+ } else {
83
+ options.ckeditor_path = 'https://cdn.ckeditor.com/ckeditor5/35.3.2/inline/ckeditor.js';
84
+ }
85
+ }
86
+
87
+ if (options.ckeditor_path) {
88
+ alchemy.exposeStatic('ckeditor_path', options.ckeditor_path);
89
+ }
90
+
91
+ if (options.ckeditor_toolbar) {
92
+ alchemy.exposeStatic('ckeditor_toolbar', options.ckeditor_toolbar);
93
+ }
94
+
7
95
  Router.add({
8
96
  name : 'AlchemyWidgets#save',
9
97
  methods : 'post',
10
98
  paths : '/api/alchemywidgets/save',
11
99
  policy : 'logged_in',
100
+ permission : 'alchemy.widgets.save',
12
101
  });
102
+
103
+ Router.add({
104
+ name : 'AlchemyWidgets#uploadImage',
105
+ methods : 'post',
106
+ paths : '/api/alchemywidgets/upload',
107
+ policy : 'logged_in',
108
+ permission : 'alchemy.widgets.image.upload',
109
+ });
@@ -184,4 +184,68 @@ AlchemyWidgets.setAction(async function save(conduit) {
184
184
  };
185
185
 
186
186
  conduit.end(result);
187
+ });
188
+
189
+ /**
190
+ * Handle an image upload from within CKeditor
191
+ *
192
+ * @author Jelle De Loecker <jelle@elevenways.be>
193
+ * @since 0.2.1
194
+ * @version 0.2.1
195
+ */
196
+ AlchemyWidgets.setAction(async function uploadImage(conduit) {
197
+
198
+ let file = conduit.files.upload;
199
+
200
+ const MediaFile = this.getModel('MediaFile');
201
+
202
+ let name = file.name.split('.');
203
+
204
+ // Remove the last piece if there are more than 1
205
+ if (name.length > 1) {
206
+ name.pop();
207
+ }
208
+
209
+ // Join them again
210
+ name = name.join('.');
211
+
212
+ const options = {
213
+ move : true,
214
+ filename : file.name,
215
+ name : name,
216
+ };
217
+
218
+ MediaFile.addFile(file.path, options, (err, result) => {
219
+
220
+ if (err) {
221
+ return conduit.error(err);
222
+ }
223
+
224
+ const params = {
225
+ id : result._id,
226
+ };
227
+
228
+ let default_path = alchemy.routeUrl('Media::image', params);
229
+ let url = RURL.parse(default_path);
230
+
231
+ let path_800 = url.clone();
232
+ path_800.param('width', '800');
233
+
234
+ let path_1024 = url.clone();
235
+ path_1024.param('height', '1024');
236
+
237
+ let path_1920 = url.clone();
238
+ path_1920.param('width', '1920');
239
+
240
+ let response = {
241
+ urls: {
242
+ default : default_path,
243
+ '800' : path_800+'',
244
+ '1024' : path_1024+'',
245
+ '1920' : path_1920+''
246
+ }
247
+ };
248
+
249
+ conduit.end(response);
250
+ });
187
251
  });
@@ -364,7 +364,7 @@ Base.setMethod(async function save() {
364
364
  *
365
365
  * @author Jelle De Loecker <jelle@elevenways.be>
366
366
  * @since 0.2.0
367
- * @version 0.2.0
367
+ * @version 0.2.1
368
368
  */
369
369
  Base.setMethod(async function copyConfigToClipboard() {
370
370
 
@@ -377,11 +377,15 @@ Base.setMethod(async function copyConfigToClipboard() {
377
377
  value._altype = 'widget';
378
378
  value.type = this.type;
379
379
 
380
+ let dried = JSON.dry(value, null, '\t');
381
+
380
382
  try {
381
- await navigator.clipboard.writeText(JSON.dry(value, null, '\t'));
383
+ await navigator.clipboard.writeText(dried);
382
384
  } catch (err) {
383
385
  console.error('Failed to copy:', err);
384
386
  }
387
+
388
+ localStorage._copied_widget_config = dried;
385
389
  });
386
390
 
387
391
  /**
@@ -389,7 +393,7 @@ Base.setMethod(async function copyConfigToClipboard() {
389
393
  *
390
394
  * @author Jelle De Loecker <jelle@elevenways.be>
391
395
  * @since 0.2.0
392
- * @version 0.2.0
396
+ * @version 0.2.1
393
397
  */
394
398
  Base.setMethod(async function getConfigFromClipboard() {
395
399
 
@@ -398,7 +402,12 @@ Base.setMethod(async function getConfigFromClipboard() {
398
402
  try {
399
403
  result = await navigator.clipboard.readText();
400
404
  } catch (err) {
401
- return false;
405
+
406
+ if (!localStorage._copied_widget_config) {
407
+ return false;
408
+ } else {
409
+ result = localStorage._copied_widget_config;
410
+ }
402
411
  }
403
412
 
404
413
  if (result) {
@@ -79,6 +79,15 @@ Widget.setAssignedProperty('filter_value');
79
79
  */
80
80
  Widget.setAttribute('child-class');
81
81
 
82
+ /**
83
+ * A role to give to each child widgets
84
+ *
85
+ * @author Jelle De Loecker <jelle@elevenways.be>
86
+ * @since 0.2.1
87
+ * @version 0.2.1
88
+ */
89
+ Widget.setAttribute('child-role');
90
+
82
91
  /**
83
92
  * Is this widget being edited?
84
93
  *
@@ -206,7 +215,7 @@ Widget.setMethod(function getValueFromRecord(record) {
206
215
  *
207
216
  * @author Jelle De Loecker <jelle@elevenways.be>
208
217
  * @since 0.1.5
209
- * @version 0.1.6
218
+ * @version 0.2.1
210
219
  */
211
220
  Widget.setMethod(function applyValue(value) {
212
221
 
@@ -246,7 +255,7 @@ Widget.setMethod(function applyValue(value) {
246
255
  }
247
256
 
248
257
  this.instance.config = config;
249
- let promise = this.instance.populateWidget();
258
+ let promise = this.instance.loadWidget();
250
259
 
251
260
  if (promise) {
252
261
  this.delayAssemble(promise);
@@ -269,13 +278,11 @@ Widget.setMethod(function syncConfig() {
269
278
  *
270
279
  * @author Jelle De Loecker <jelle@elevenways.be>
271
280
  * @since 0.1.0
272
- * @version 0.1.0
281
+ * @version 0.2.1
273
282
  */
274
283
  Widget.setMethod(function startEditor() {
275
284
 
276
- if (!this.instance) {
277
- throw new Error('Unable to start the editor: this widget element has no accompanying instance');
278
- }
285
+ this.assertWidgetInstance();
279
286
 
280
287
  this.instance.startEditor();
281
288
  this.addEditEventListeners();
@@ -286,19 +293,33 @@ Widget.setMethod(function startEditor() {
286
293
  *
287
294
  * @author Jelle De Loecker <jelle@elevenways.be>
288
295
  * @since 0.1.0
289
- * @version 0.1.5
296
+ * @version 0.2.1
290
297
  */
291
298
  Widget.setMethod(function stopEditor() {
292
299
 
293
- if (!this.instance) {
294
- throw new Error('Unable to stop the editor: this widget element has no accompanying instance');
295
- }
300
+ this.assertWidgetInstance();
296
301
 
297
302
  this.unselectWidget();
298
303
  this.instance.stopEditor();
299
304
  this.removeEditEventListeners();
300
305
  });
301
306
 
307
+ /**
308
+ * Make sure there is an instance
309
+ *
310
+ * @author Jelle De Loecker <jelle@elevenways.be>
311
+ * @since 0.2.1
312
+ * @version 0.2.1
313
+ */
314
+ Widget.setMethod(function assertWidgetInstance() {
315
+
316
+ if (!this.instance) {
317
+ console.error('Problem with widget element:', this);
318
+ throw new Error('Unable to stop the editor: this ' + this.tagName + ' element has no accompanying instance');
319
+ }
320
+
321
+ });
322
+
302
323
  /**
303
324
  * Mouse click
304
325
  *
@@ -62,21 +62,22 @@ AlchemyWidgets.setAssignedProperty('context_variables', function getContextData(
62
62
  *
63
63
  * @author Jelle De Loecker <jelle@elevenways.be>
64
64
  * @since 0.1.0
65
- * @version 0.1.0
65
+ * @version 0.2.1
66
66
  */
67
67
  AlchemyWidgets.setProperty(function value() {
68
68
 
69
- let widgets = this.getWidgetsConfig(),
69
+ let config = this.instance.config,
70
+ widgets = this.getWidgetsConfig(),
70
71
  result;
72
+
73
+ config = Object.assign({}, config, {widgets});
71
74
 
72
75
  if (this.nodeName == 'AL-WIDGETS') {
73
- result = {widgets};
76
+ result = config;
74
77
  } else {
75
78
  result = {
76
79
  type : this.instance.constructor.type_name,
77
- config : {
78
- widgets : widgets
79
- }
80
+ config : config,
80
81
  };
81
82
  }
82
83
 
@@ -91,7 +92,7 @@ AlchemyWidgets.setProperty(function value() {
91
92
  *
92
93
  * @author Jelle De Loecker <jelle@elevenways.be>
93
94
  * @since 0.1.5
94
- * @version 0.1.5
95
+ * @version 0.2.1
95
96
  */
96
97
  AlchemyWidgets.setMethod(function applyValue(value) {
97
98
 
@@ -119,24 +120,18 @@ AlchemyWidgets.setMethod(function applyValue(value) {
119
120
  if (config.class_names) {
120
121
  Hawkejs.addClasses(this, config.class_names);
121
122
  }
123
+ } else {
124
+ config = this.instance?.config || {};
122
125
  }
123
126
 
124
127
  if (!this.instance) {
125
128
  return;
126
129
  }
127
130
 
131
+ config.widgets = widgets;
132
+
128
133
  this.instance.config = config;
129
134
  this.instance.initContainer();
130
-
131
- if (!widgets || !widgets.length) {
132
- return;
133
- }
134
-
135
- let widget;
136
-
137
- for (widget of widgets) {
138
- this.addWidget(widget.type, widget.config);
139
- }
140
135
  });
141
136
 
142
137
  /**
@@ -88,12 +88,21 @@ TableOfContents.setAttribute('title-selector');
88
88
  */
89
89
  TableOfContents.setAttribute('intersection-class');
90
90
 
91
+ /**
92
+ * Should titles be truncated?
93
+ *
94
+ * @author Jelle De Loecker <jelle@elevenways.be>
95
+ * @since 0.2.1
96
+ * @version 0.2.1
97
+ */
98
+ TableOfContents.setAttribute('truncate-length', {type: 'number'});
99
+
91
100
  /**
92
101
  * Get the entries
93
102
  *
94
103
  * @author Jelle De Loecker <jelle@elevenways.be>
95
104
  * @since 0.1.2
96
- * @version 0.2.0
105
+ * @version 0.2.1
97
106
  */
98
107
  TableOfContents.setProperty(function entries() {
99
108
 
@@ -138,7 +147,7 @@ TableOfContents.setProperty(function entries() {
138
147
 
139
148
  let title_element,
140
149
  starts_level,
141
- ends_level;
150
+ ends_level;
142
151
 
143
152
  if (this.title_selector) {
144
153
  title_element = element.querySelector(this.title_selector);
@@ -155,7 +164,10 @@ TableOfContents.setProperty(function entries() {
155
164
  current_level = heading_level;
156
165
  } else if (heading_level > current_level) {
157
166
  current_level++;
158
- starts_level = true;
167
+
168
+ if (last_entry) {
169
+ last_entry.starts_level = true;
170
+ }
159
171
  } else if (heading_level == current_level && last_entry) {
160
172
  last_entry.starts_level = false;
161
173
  } else if (heading_level < current_level) {
@@ -169,7 +181,9 @@ TableOfContents.setProperty(function entries() {
169
181
 
170
182
  title = (title_element.toc_title || title_element.textContent || '').trim();
171
183
 
172
- title = title.truncate(30);
184
+ if (this.truncate_length) {
185
+ title = title.truncate(this.truncate_length);
186
+ }
173
187
 
174
188
  // Don't add empty titles
175
189
  if (!title) {
@@ -191,7 +205,7 @@ TableOfContents.setProperty(function entries() {
191
205
  let ended_level = false,
192
206
  entries = result,
193
207
  current_branch,
194
- current_nodes = [];
208
+ current_nodes = [];
195
209
 
196
210
  result = current_nodes;
197
211
  last_entry = null;
@@ -255,7 +269,7 @@ TableOfContents.setProperty(function entries() {
255
269
  *
256
270
  * @author Jelle De Loecker <jelle@elevenways.be>
257
271
  * @since 0.1.2
258
- * @version 0.1.2
272
+ * @version 0.2.1
259
273
  */
260
274
  TableOfContents.setMethod(async function introduced() {
261
275
 
@@ -263,7 +277,8 @@ TableOfContents.setMethod(async function introduced() {
263
277
 
264
278
  const observer = new IntersectionObserver(entries => {
265
279
 
266
- let class_name = this.intersection_class || 'visible';
280
+ let class_name = this.intersection_class || 'visible',
281
+ first_name = class_name + '-first';
267
282
 
268
283
  for (let entry of entries) {
269
284
  const id = entry.target.getAttribute('id');
@@ -281,6 +296,28 @@ TableOfContents.setMethod(async function introduced() {
281
296
  element.classList.remove(class_name);
282
297
  }
283
298
  };
299
+
300
+ let is_visible,
301
+ all_marked = this.querySelectorAll('.' + class_name + ', .' + first_name),
302
+ element,
303
+ seen = 0,
304
+ i;
305
+
306
+ for (i = 0; i < all_marked.length; i++) {
307
+ element = all_marked[i];
308
+ is_visible = element.classList.contains(class_name);
309
+
310
+ if (is_visible && seen == 0) {
311
+ element.classList.add(first_name);
312
+ } else {
313
+ element.classList.remove(first_name);
314
+ }
315
+
316
+ if (is_visible) {
317
+ seen++;
318
+ }
319
+ }
320
+
284
321
  });
285
322
 
286
323
  for (let entry of this.entries) {