alchemy-widget 0.2.0 → 0.2.1

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,23 @@
1
+ ## 0.2.1 (2022-12-23)
2
+
3
+ * Add a `-first` modifier class to the first visible entry of a table-of-contents element
4
+ * Make HTML widget editable
5
+ * Also store and retrieve copied widget config from localStorage
6
+ * Move `Container` widget init logic from the custom-element to the main widget instance
7
+ * Disable most `backdrop-filter` properties, they caused too many FPS issues
8
+ * Add the ability to hide widgets from the public
9
+ * Allow overriding the language (`lang` attribute) of a widget
10
+ * Fix config of container widgets not being saved
11
+ * Only add minium dimensions when editing
12
+ * Add `child-role` attribute & use it to set the default role of child widgets
13
+ * Add the `toc-has-content` or `toc-is-empty` class to `al-toc` elements
14
+ * Fix issue where `Text` widget would break when edited with LanguageTool extension
15
+ * Do not truncate titles in table-of-contents automatically
16
+ * Fix nesting levels in `al-toc`
17
+ * Only allow text inside header widgets
18
+ * Move default `Widget#populateWidget()` code to `Widget#finalizePopulatedWidget()`
19
+ * Add `HawkejsTemplate` widget
20
+
1
21
  ## 0.2.0 (2022-11-02)
2
22
 
3
23
  * 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
  *
@@ -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) {
@@ -5,14 +5,18 @@
5
5
  *
6
6
  * @author Jelle De Loecker <jelle@elevenways.be>
7
7
  * @since 0.1.0
8
- * @version 0.1.0
8
+ * @version 0.2.1
9
9
  *
10
10
  * @param {Object} config
11
11
  */
12
12
  const Widget = Function.inherits('Alchemy.Base', 'Alchemy.Widget', function Widget(config) {
13
13
 
14
14
  // The configuration of this widget
15
- this.config = config || {};
15
+ if (config) {
16
+ this.config = config;
17
+ }
18
+
19
+ this.originalconfig = this.config;
16
20
 
17
21
  // Are we currently editing?
18
22
  this.editing = false;
@@ -72,6 +76,24 @@ Widget.setProperty(function element() {
72
76
  return this.widget = element;
73
77
  });
74
78
 
79
+ /**
80
+ * The config object
81
+ *
82
+ * @author Jelle De Loecker <jelle@elevenways.be>
83
+ * @since 0.2.1
84
+ * @version 0.2.1
85
+ *
86
+ * @type {Object}
87
+ */
88
+ Widget.enforceProperty(function config(new_value) {
89
+
90
+ if (!new_value) {
91
+ new_value = {};
92
+ }
93
+
94
+ return new_value;
95
+ });
96
+
75
97
  /**
76
98
  * A reference to the renderer
77
99
  *
@@ -96,12 +118,37 @@ Widget.enforceProperty(function hawkejs_renderer(new_value) {
96
118
  return new_value;
97
119
  });
98
120
 
121
+ /**
122
+ * Visibility of the widget
123
+ *
124
+ * @author Jelle De Loecker <jelle@elevenways.be>
125
+ * @since 0.2.1
126
+ * @version 0.2.1
127
+ *
128
+ * @type {Boolean}
129
+ */
130
+ Widget.enforceProperty(function is_hidden(new_value) {
131
+
132
+ if (new_value == null) {
133
+ new_value = this.config.hidden;
134
+
135
+ if (new_value == null) {
136
+ new_value = false;
137
+ }
138
+ } else {
139
+ this.config.hidden = new_value;
140
+ this.queueVisibilityUpdate();
141
+ }
142
+
143
+ return new_value;
144
+ });
145
+
99
146
  /**
100
147
  * Prepare the schema & actions
101
148
  *
102
149
  * @author Jelle De Loecker <jelle@elevenways.be>
103
150
  * @since 0.1.0
104
- * @version 0.2.0
151
+ * @version 0.2.1
105
152
  */
106
153
  Widget.constitute(function prepareSchema() {
107
154
 
@@ -126,6 +173,19 @@ Widget.constitute(function prepareSchema() {
126
173
  array: true,
127
174
  });
128
175
 
176
+ // Should this widget be hidden?
177
+ this.schema.addField('hidden', 'Boolean', {
178
+ title : 'Should this widget be hidden?',
179
+ description : 'Hidden widgets are only visible during editing',
180
+ default : false,
181
+ });
182
+
183
+ this.schema.addField('language', 'String', {
184
+ title : 'Language override',
185
+ description : 'If the content of this widget is in a different language, set it here',
186
+ widget_config_editable : true,
187
+ });
188
+
129
189
  // Add the "copy to clipboard" action
130
190
  let copy = this.createAction('copy', 'Copy to clipboard');
131
191
 
@@ -167,6 +227,36 @@ Widget.constitute(function prepareSchema() {
167
227
 
168
228
  save.setIcon('floppy-disk');
169
229
 
230
+ // Add the hide action
231
+ let hide = this.createAction('hide', 'Hide');
232
+
233
+ hide.close_actionbar = true;
234
+
235
+ hide.setHandler(function hideAction(widget_el, handle) {
236
+ widget_el.instance.is_hidden = true;
237
+ });
238
+
239
+ hide.setTester(function hideTester(widget_el, handle) {
240
+ return !widget_el.instance.is_hidden;
241
+ });
242
+
243
+ hide.setIcon('eye-slash');
244
+
245
+ // Add the show action
246
+ let show = this.createAction('show', 'Show');
247
+
248
+ show.close_actionbar = true;
249
+
250
+ show.setHandler(function showAction(widget_el, handle) {
251
+ widget_el.instance.is_hidden = false;
252
+ });
253
+
254
+ show.setTester(function showTester(widget_el, handle) {
255
+ return widget_el.instance.is_hidden;
256
+ });
257
+
258
+ show.setIcon('eye');
259
+
170
260
  // Add the remove action
171
261
  let remove = this.createAction('remove', 'Remove');
172
262
 
@@ -559,20 +649,34 @@ Widget.setMethod(function _createPopulatedWidgetElement() {
559
649
  return element;
560
650
  });
561
651
 
652
+
653
+ /**
654
+ * Populate the actual widget
655
+ *
656
+ * @author Jelle De Loecker <jelle@elevenways.be>
657
+ * @since 0.2.1
658
+ * @version 0.2.1
659
+ */
660
+ Widget.setMethod(function populateWidget() {
661
+ // Does nothing on its own
662
+ });
663
+
562
664
  /**
563
665
  * Populate the contents of the widget
564
666
  *
565
667
  * @author Jelle De Loecker <jelle@elevenways.be>
566
668
  * @since 0.1.0
567
- * @version 0.1.6
669
+ * @version 0.2.1
568
670
  */
569
- Widget.setMethod(function populateWidget() {
671
+ Widget.setMethod(function finalizePopulatedWidget() {
672
+
673
+ const config = this.config;
570
674
 
571
- if (this.config && this.config.wrapper_class_names) {
675
+ if (config.wrapper_class_names) {
572
676
  let name,
573
677
  i;
574
678
 
575
- let class_names = Array.cast(this.config.wrapper_class_names);
679
+ let class_names = Array.cast(config.wrapper_class_names);
576
680
 
577
681
  for (i = 0; i < class_names.length; i++) {
578
682
  name = class_names[i];
@@ -580,6 +684,12 @@ Widget.setMethod(function populateWidget() {
580
684
  }
581
685
  }
582
686
 
687
+ if (config.language) {
688
+ this.widget.setAttribute('lang', config.language);
689
+ } else {
690
+ this.widget.removeAttribute('lang');
691
+ }
692
+
583
693
  let child_classes = this.widget.child_class;
584
694
 
585
695
  if (child_classes) {
@@ -590,6 +700,68 @@ Widget.setMethod(function populateWidget() {
590
700
  Hawkejs.addClasses(children[i], child_classes);
591
701
  }
592
702
  }
703
+
704
+ let child_roles = this.widget.child_role;
705
+
706
+ if (child_roles) {
707
+ let children = this.widget.children,
708
+ child,
709
+ i;
710
+
711
+ for (i = 0; i < children.length; i++) {
712
+ child = children[i];
713
+
714
+ if (!child.hasAttribute('role')) {
715
+ child.setAttribute('role', child_roles);
716
+ }
717
+ }
718
+ }
719
+
720
+ this.checkVisibility();
721
+ });
722
+
723
+ /**
724
+ * Queue a widget element visibility update
725
+ *
726
+ * @author Jelle De Loecker <jelle@elevenways.be>
727
+ * @since 0.2.1
728
+ * @version 0.2.1
729
+ */
730
+ Widget.setMethod(function queueVisibilityUpdate() {
731
+
732
+ if (this._visibility_update_queue) {
733
+ clearTimeout(this._visibility_update_queue);
734
+ }
735
+
736
+ this._visibility_update_queue = setTimeout(() => {
737
+ this.checkVisibility();
738
+ this._visibility_update_queue = null;
739
+ }, 50);
740
+ });
741
+
742
+ /**
743
+ * Check the widget element visibility
744
+ *
745
+ * @author Jelle De Loecker <jelle@elevenways.be>
746
+ * @since 0.2.1
747
+ * @version 0.2.1
748
+ */
749
+ Widget.setMethod(function checkVisibility() {
750
+
751
+ let should_be_hidden = this.is_hidden;
752
+
753
+ if (this.editing) {
754
+ this.widget.hidden = false;
755
+ } else {
756
+ this.widget.hidden = should_be_hidden;
757
+ }
758
+
759
+ if (should_be_hidden) {
760
+ this.widget.classList.add('aw-hidden');
761
+ } else {
762
+ this.widget.classList.remove('aw-hidden');
763
+ }
764
+
593
765
  });
594
766
 
595
767
  /**
@@ -597,7 +769,7 @@ Widget.setMethod(function populateWidget() {
597
769
  *
598
770
  * @author Jelle De Loecker <jelle@elevenways.be>
599
771
  * @since 0.1.0
600
- * @version 0.1.6
772
+ * @version 0.2.1
601
773
  */
602
774
  Widget.setMethod(async function startEditor() {
603
775
 
@@ -617,6 +789,8 @@ Widget.setMethod(async function startEditor() {
617
789
  if (typeof this._startEditor == 'function') {
618
790
  this._startEditor();
619
791
  }
792
+
793
+ this.checkVisibility();
620
794
  });
621
795
 
622
796
  /**
@@ -624,7 +798,7 @@ Widget.setMethod(async function startEditor() {
624
798
  *
625
799
  * @author Jelle De Loecker <jelle@elevenways.be>
626
800
  * @since 0.1.0
627
- * @version 0.1.0
801
+ * @version 0.2.1
628
802
  */
629
803
  Widget.setMethod(function stopEditor() {
630
804
 
@@ -638,7 +812,13 @@ Widget.setMethod(function stopEditor() {
638
812
 
639
813
  if (typeof this._stopEditor == 'function') {
640
814
  this._stopEditor();
815
+
816
+ // Remove the editing class again
817
+ // (some editors will try to restore the original classes)
818
+ this.widget.classList.remove('aw-editing');
641
819
  }
820
+
821
+ this.checkVisibility();
642
822
  });
643
823
 
644
824
  /**
@@ -646,13 +826,14 @@ Widget.setMethod(function stopEditor() {
646
826
  *
647
827
  * @author Jelle De Loecker <jelle@elevenways.be>
648
828
  * @since 0.1.0
649
- * @version 0.1.6
829
+ * @version 0.2.1
650
830
  */
651
831
  Widget.setMethod(async function rerender() {
652
832
 
653
833
  Hawkejs.removeChildren(this.widget);
654
834
 
655
835
  await this.populateWidget();
836
+ await this.finalizePopulatedWidget();
656
837
 
657
838
  if (this.editing) {
658
839
  this.startEditor();
@@ -46,6 +46,28 @@ Container.setMethod(function initContainer() {
46
46
  this.populateWidget();
47
47
  });
48
48
 
49
+ /**
50
+ * Populate the widget
51
+ *
52
+ * @author Jelle De Loecker <jelle@elevenways.be>
53
+ * @since 0.2.1
54
+ * @version 0.2.1
55
+ *
56
+ * @return {HTMLElement}
57
+ */
58
+ Container.setMethod(function populateWidget() {
59
+
60
+ const widgets = this.config.widgets;
61
+
62
+ if (widgets?.length) {
63
+ let widget;
64
+
65
+ for (widget of widgets) {
66
+ this.widget.addWidget(widget.type, widget.config);
67
+ }
68
+ }
69
+ });
70
+
49
71
  /**
50
72
  * Create an instance of the HTML element representing this widget
51
73
  *
@@ -133,8 +133,6 @@ Tabs.setMethod(function populateWidget() {
133
133
  }
134
134
 
135
135
  Hawkejs.replaceChildren(this.widget, wrapper);
136
-
137
- return populateWidget.super.call(this);
138
136
  });
139
137
 
140
138
  /**
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The Widget Template class
3
+ *
4
+ * @constructor
5
+ *
6
+ * @author Jelle De Loecker <jelle@elevenways.be>
7
+ * @since 0.2.1
8
+ * @version 0.2.1
9
+ *
10
+ * @param {Object} data
11
+ */
12
+ const Template = Function.inherits('Alchemy.Widget.Sourcecode', 'HawkejsTemplate');
13
+
14
+ /**
15
+ * Populate the widget
16
+ *
17
+ * @author Jelle De Loecker <jelle@elevenways.be>
18
+ * @since 0.2.1
19
+ * @version 0.2.1
20
+ *
21
+ * @param {HTMLElement} widget
22
+ */
23
+ Template.setMethod(async function populateWidget() {
24
+
25
+ let input = this.config.sourcecode;
26
+
27
+ if (input) {
28
+ input = input.trim();
29
+ }
30
+
31
+ Hawkejs.removeChildren(this.widget);
32
+
33
+ if (input) {
34
+
35
+ let hawkejs = this.hawkejs_renderer.hawkejs,
36
+ hash = Object.checksum(input),
37
+ name = 'interpret_' + hash,
38
+ fnc;
39
+
40
+ if (hawkejs.templates[name]) {
41
+ fnc = hawkejs.templates[name];
42
+ } else {
43
+ fnc = hawkejs.compile({
44
+ template_name : name,
45
+ template : input
46
+ });
47
+ }
48
+
49
+ let variables = {};
50
+ let placeholder = this.hawkejs_renderer.addSubtemplate(fnc, {print: false}, variables);
51
+
52
+ // If the widget is already part of the DOM,
53
+ // it's being edited and we need to manually kickstart the renderer
54
+ if (Blast.isBrowser && document.body.contains(this.widget)) {
55
+ await placeholder.getContent();
56
+ }
57
+
58
+ this.widget.append(placeholder);
59
+ }
60
+ });
@@ -78,18 +78,18 @@ Header.constitute(function addActions() {
78
78
  *
79
79
  * @author Jelle De Loecker <jelle@elevenways.be>
80
80
  * @since 0.1.0
81
- * @version 0.1.0
81
+ * @version 0.2.1
82
82
  *
83
83
  * @return {Object}
84
84
  */
85
85
  Header.setMethod(function syncConfig() {
86
86
 
87
- let header = this.widget.children[0],
87
+ let header = this.widget.querySelector('h1, h2, h3, h4, h5'),
88
88
  content = '',
89
89
  level = 1;
90
90
 
91
91
  if (header) {
92
- content = header.innerHTML;
92
+ content = header.textContent;
93
93
  level = parseInt(header.tagName[1]);
94
94
  }
95
95
 
@@ -104,7 +104,7 @@ Header.setMethod(function syncConfig() {
104
104
  *
105
105
  * @author Jelle De Loecker <jelle@elevenways.be>
106
106
  * @since 0.1.0
107
- * @version 0.1.0
107
+ * @version 0.2.1
108
108
  *
109
109
  * @param {HTMLElement} widget
110
110
  */
@@ -113,11 +113,9 @@ Header.setMethod(function populateWidget() {
113
113
  let level = this.config.level || 1;
114
114
 
115
115
  let header = this.createElement('h' + level);
116
- header.innerHTML = this.config.content || 'header level ' + level;
116
+ header.textContent = this.config.content || 'header level ' + level;
117
117
 
118
118
  this.widget.append(header);
119
-
120
- populateWidget.super.call(this);
121
119
  });
122
120
 
123
121
  /**
@@ -1,3 +1,4 @@
1
+
1
2
  /**
2
3
  * The Widget HTML class
3
4
  *
@@ -23,19 +24,67 @@ Html.constitute(function prepareSchema() {
23
24
  this.schema.addField('html', 'Html');
24
25
  });
25
26
 
27
+ /**
28
+ * Start the editor
29
+ *
30
+ * @author Jelle De Loecker <jelle@elevenways.be>
31
+ * @since 0.2.1
32
+ * @version 0.2.1
33
+ */
34
+ Html.setMethod(async function _startEditor() {
35
+
36
+ if (this.ckeditor) {
37
+ this.ckeditor.destroy();
38
+ }
39
+
40
+ let ckeditor_path = hawkejs.scene.exposed.ckeditor_path;
41
+
42
+ if (!ckeditor_path) {
43
+ return;
44
+ }
45
+
46
+ await hawkejs.require(ckeditor_path);
47
+
48
+ const options = {
49
+ toolbar: hawkejs.scene.exposed.ckeditor_toolbar,
50
+ updateSourceElementOnDestroy: true,
51
+ removePlugins: ['Markdown'],
52
+ simpleUpload: {
53
+ uploadUrl: alchemy.routeUrl('AlchemyWidgets#uploadImage'),
54
+ withCredentials: true,
55
+ },
56
+ };
57
+
58
+ let editor = await InlineEditor.create(this.widget, options);
59
+
60
+ this.ckeditor = editor;
61
+ });
62
+
63
+ /**
64
+ * Stop the editor
65
+ *
66
+ * @author Jelle De Loecker <jelle@elevenways.be>
67
+ * @since 0.2.1
68
+ * @version 0.2.1
69
+ */
70
+ Html.setMethod(function _stopEditor() {
71
+ if (this.ckeditor) {
72
+ this.ckeditor.destroy();
73
+ this.ckeditor = null;
74
+ }
75
+ });
76
+
26
77
  /**
27
78
  * Populate the widget
28
79
  *
29
80
  * @author Jelle De Loecker <jelle@elevenways.be>
30
81
  * @since 0.1.0
31
- * @version 0.1.0
82
+ * @version 0.2.1
32
83
  *
33
84
  * @param {HTMLElement} widget
34
85
  */
35
86
  Html.setMethod(function populateWidget() {
36
87
  this.widget.innerHTML = this.config.html;
37
-
38
- populateWidget.super.call(this);
39
88
  });
40
89
 
41
90
  /**
@@ -28,14 +28,12 @@ Markdown.constitute(function prepareSchema() {
28
28
  *
29
29
  * @author Jelle De Loecker <jelle@elevenways.be>
30
30
  * @since 0.1.0
31
- * @version 0.1.0
31
+ * @version 0.2.1
32
32
  *
33
33
  * @param {HTMLElement} widget
34
34
  */
35
35
  Markdown.setMethod(function populateWidget() {
36
36
 
37
- populateWidget.super.call(this);
38
-
39
37
  let source = this.config.markdown || '';
40
38
 
41
39
  if (!source) {
@@ -67,7 +67,7 @@ Partial.constitute(function prepareSchema() {
67
67
  *
68
68
  * @author Jelle De Loecker <jelle@elevenways.be>
69
69
  * @since 0.1.6
70
- * @version 0.1.6
70
+ * @version 0.2.1
71
71
  *
72
72
  * @param {HTMLElement} widget
73
73
  */
@@ -105,8 +105,6 @@ Partial.setMethod(async function populateWidget() {
105
105
 
106
106
  this.widget.append(placeholder);
107
107
  }
108
-
109
- populateWidget.super.call(this);
110
108
  });
111
109
 
112
110
  /**
@@ -37,14 +37,12 @@ Sourcecode.setProperty('sourcecode_field', 'sourcecode');
37
37
  *
38
38
  * @author Jelle De Loecker <jelle@elevenways.be>
39
39
  * @since 0.1.0
40
- * @version 0.1.0
40
+ * @version 0.2.1
41
41
  *
42
42
  * @param {HTMLElement} widget
43
43
  */
44
44
  Sourcecode.setMethod(function populateWidget() {
45
45
 
46
- populateWidget.super.call(this);
47
-
48
46
  let source = this.config[this.sourcecode_field] || '';
49
47
 
50
48
  if (!source) {
@@ -16,12 +16,10 @@ const Toc = Function.inherits('Alchemy.Widget', 'TableOfContents');
16
16
  *
17
17
  * @author Jelle De Loecker <jelle@elevenways.be>
18
18
  * @since 0.1.2
19
- * @version 0.2.0
19
+ * @version 0.2.1
20
20
  */
21
21
  Toc.setMethod(function populateWidget() {
22
22
 
23
- populateWidget.super.call(this);
24
-
25
23
  let toc = this.createElement('al-toc');
26
24
 
27
25
  if (this.config.parent_selector) {
@@ -28,7 +28,7 @@ Text.constitute(function prepareSchema() {
28
28
  *
29
29
  * @author Jelle De Loecker <jelle@elevenways.be>
30
30
  * @since 0.1.0
31
- * @version 0.1.6
31
+ * @version 0.2.1
32
32
  *
33
33
  * @param {HTMLElement} widget
34
34
  */
@@ -36,8 +36,6 @@ Text.setMethod(function populateWidget() {
36
36
 
37
37
  let tag_name;
38
38
 
39
- populateWidget.super.call(this);
40
-
41
39
  if (this.widget.dataset.textElementTag) {
42
40
  tag_name = this.widget.dataset.textElementTag;
43
41
  }
@@ -57,13 +55,16 @@ Text.setMethod(function populateWidget() {
57
55
  *
58
56
  * @author Jelle De Loecker <jelle@elevenways.be>
59
57
  * @since 0.1.0
60
- * @version 0.1.0
58
+ * @version 0.2.1
61
59
  */
62
60
  Text.setMethod(function _startEditor() {
63
61
 
64
- let child = this.widget.children[0];
62
+ let child,
63
+ i;
64
+
65
+ for (i = 0; i < this.widget.children.length; i++) {
66
+ child = this.widget.children[i];
65
67
 
66
- if (child) {
67
68
  child.setAttribute('contenteditable', true);
68
69
  }
69
70
  });
@@ -73,13 +74,16 @@ Text.setMethod(function _startEditor() {
73
74
  *
74
75
  * @author Jelle De Loecker <jelle@elevenways.be>
75
76
  * @since 0.1.0
76
- * @version 0.1.0
77
+ * @version 0.2.1
77
78
  */
78
79
  Text.setMethod(function _stopEditor() {
79
80
 
80
- let child = this.widget.children[0];
81
+ let child,
82
+ i;
83
+
84
+ for (i = 0; i < this.widget.children.length; i++) {
85
+ child = this.widget.children[i];
81
86
 
82
- if (child) {
83
87
  child.removeAttribute('contenteditable');
84
88
  }
85
89
  });
@@ -89,17 +93,13 @@ Text.setMethod(function _stopEditor() {
89
93
  *
90
94
  * @author Jelle De Loecker <jelle@elevenways.be>
91
95
  * @since 0.1.0
92
- * @version 0.1.0
96
+ * @version 0.2.1
93
97
  *
94
98
  * @return {Object}
95
99
  */
96
100
  Text.setMethod(function syncConfig() {
97
101
 
98
- let child = this.widget.children[0];
99
-
100
- if (child) {
101
- this.config.content = child.textContent;
102
- }
102
+ this.config.content = this.widget?.textContent;
103
103
 
104
104
  return this.config;
105
105
  });
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.2.0",
4
+ "version": "0.2.1",
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.2.0",
14
- "alchemy-form": "~0.2.0"
14
+ "alchemy-form": "~0.2.1"
15
15
  },
16
16
  "repository": "11ways/alchemy-widget",
17
17
  "license": "MIT",
@@ -1,3 +1,5 @@
1
+ <% all_entries = self.entries %>
2
+
1
3
  {% macro printEntries %}
2
4
  <ol>
3
5
  {% each entries as entry %}
@@ -20,4 +22,12 @@
20
22
  </li>
21
23
  {% /macro %}
22
24
 
23
- {% run printEntries entries=self.entries}
25
+ {% run printEntries entries=all_entries %}
26
+
27
+ {% if all_entries %}
28
+ <% self.classList.add('toc-has-content') %>
29
+ {% else %}
30
+ <% self.classList.add('toc-is-empty') %>
31
+ {% /if %}
32
+
33
+ <% all_entries = null %>