Skip to content

Commit ff98169

Browse files
authored
PRO-7380 PRO-7483 Widget live preview (#4893)
* live preview wip * working, needs some touches & cypress * removed unused code, unrelated * fixed bugs with "add widget" * guess the origin * cleaned up * experimenting with fixing flicker in video step 1 * much better video widget rendering all around, but still too flickery in preview * changelog * quiet * fixed unused variable * progress with the tests but still problems * wip * backed out use of placeholders for image widgets during preview, that logic is more complex tha nexpected * eslint sigh * better handling of invalid schemas * fix lint
1 parent bdc04f8 commit ff98169

File tree

13 files changed

+206
-76
lines changed

13 files changed

+206
-76
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@
22

33
## UNRELEASED
44

5+
### Adds
6+
7+
* To display a live preview on the page as changes are made to widgets, set the `preview: true` option on any widget module. To turn it on for all widgets, you can set it on the `@apostrophecms/widget-type` module, the base class of all widget modules. This works especially well when `range` fields are used to achieve visual effects.
8+
59
### Changes
610

711
* Improve the Page Manager experience when dragging and dropping pages - the updates happen in background and the UI is not blocked anymore.
812
* Allow scrolling while dragging a page in the Page Manager.
913
* Change user's email field type to `email`.
1014
* Improve media manager experience after uploading images. No additional server requests are made, no broken UI on error.
1115
* Change reset password form button label to `Reset Password`.
16+
* Removed overly verbose logging of schema errors in the schema module itself. These are already logged appropriately if they become the actual result of an API call. With this change it becomes possible to catch and discard or mitigate these in some situations without excessive log output.
1217
* Bumps eslint-config-apostrophe, fix errors and a bunch of warnings.
1318

1419
### Fixes

modules/@apostrophecms/area/index.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ module.exports = {
3737
const type = self.apos.launder.string(req.body.type);
3838
const _docId = self.apos.launder.id(req.body._docId);
3939
const field = self.apos.schema.getFieldById(areaFieldId);
40-
40+
const livePreview = self.apos.launder.boolean(req.body.livePreview);
4141
if (!field) {
4242
throw self.apos.error('invalid');
4343
}
@@ -51,7 +51,14 @@ module.exports = {
5151
self.warnMissingWidgetType(type);
5252
throw self.apos.error('invalid');
5353
}
54-
widget = await sanitize(widget);
54+
try {
55+
widget = await sanitize(widget);
56+
} catch (e) {
57+
if (livePreview) {
58+
return 'aposLivePreviewSchemaNotYetValid';
59+
}
60+
throw e;
61+
}
5562
widget._edit = true;
5663
widget._docId = _docId;
5764
// So that carrying out relationship loading again can yield results
@@ -634,6 +641,7 @@ module.exports = {
634641
const widgetEditors = {};
635642
const widgetManagers = {};
636643
const widgetIsContextual = {};
644+
const widgetPreview = {};
637645
const widgetHasPlaceholder = {};
638646
const widgetHasInitialModal = {};
639647
const contextualWidgetDefaultData = {};
@@ -645,6 +653,7 @@ module.exports = {
645653
widgetEditors[name] = (browserData && browserData.components && browserData.components.widgetEditor) || 'AposWidgetEditor';
646654
widgetManagers[name] = manager.__meta.name;
647655
widgetIsContextual[name] = manager.options.contextual;
656+
widgetPreview[name] = manager.options.preview;
648657
widgetHasPlaceholder[name] = manager.options.placeholder;
649658
widgetHasInitialModal[name] = !manager.options.placeholder && manager.options.initialModal !== false;
650659
contextualWidgetDefaultData[name] = manager.options.defaultData || {};
@@ -659,6 +668,7 @@ module.exports = {
659668
widgetIsContextual,
660669
widgetHasPlaceholder,
661670
widgetHasInitialModal,
671+
widgetPreview,
662672
contextualWidgetDefaultData,
663673
widgetManagers,
664674
action: self.action

modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue

+33-14
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@ export default {
197197
}
198198
},
199199
watch: {
200+
// Note: please don't make this a deep watcher as that could cause
201+
// issues with live widget preview and also performance, the top level
202+
// array will change in situations where a patch API call is actually
203+
// needed at this level
200204
next() {
201205
if (!this.docId) {
202206
// For the benefit of AposInputArea which is the
@@ -304,8 +308,8 @@ export default {
304308
...this.next.slice(i + 2)
305309
];
306310
},
307-
async remove(i) {
308-
if (this.docId === window.apos.adminBar.contextId) {
311+
async remove(i, { autosave = true } = {}) {
312+
if (autosave && (this.docId === window.apos.adminBar.contextId)) {
309313
apos.bus.$emit('context-edited', {
310314
$pullAllById: {
311315
[`@${this.id}.items`]: [ this.next[i]._id ]
@@ -382,13 +386,15 @@ export default {
382386
const componentName = this.widgetEditorComponent(widget.type);
383387
apos.area.activeEditor = this;
384388
apos.bus.$on('apos-refreshing', cancelRefresh);
389+
const preview = this.widgetPreview(widget.type, i, false);
385390
const result = await apos.modal.execute(componentName, {
386391
modelValue: widget,
387392
options: this.widgetOptionsByType(widget.type),
388393
type: widget.type,
389394
docId: this.docId,
390395
parentFollowingValues: this.followingValues,
391-
meta: this.meta[widget._id]?.aposMeta
396+
meta: this.meta[widget._id]?.aposMeta,
397+
preview
392398
});
393399
apos.area.activeEditor = null;
394400
apos.bus.$off('apos-refreshing', cancelRefresh);
@@ -433,12 +439,12 @@ export default {
433439
// actual files, and the reference count will update automatically
434440
}
435441
},
436-
async update(widget) {
442+
async update(widget, { autosave = true } = {}) {
437443
widget.aposPlaceholder = false;
438444
if (!widget.metaType) {
439445
widget.metaType = 'widget';
440446
}
441-
if (this.docId === window.apos.adminBar.contextId) {
447+
if (autosave && (this.docId === window.apos.adminBar.contextId)) {
442448
apos.bus.$emit('context-edited', {
443449
[`@${widget._id}`]: widget
444450
});
@@ -489,12 +495,14 @@ export default {
489495
} else {
490496
const componentName = this.widgetEditorComponent(name);
491497
apos.area.activeEditor = this;
498+
const preview = this.widgetPreview(name, index, true);
492499
const widget = await apos.modal.execute(componentName, {
493500
modelValue: null,
494501
options: this.widgetOptionsByType(name),
495502
type: name,
496503
docId: this.docId,
497-
parentFollowingValues: this.followingValues
504+
parentFollowingValues: this.followingValues,
505+
preview
498506
});
499507
apos.area.activeEditor = null;
500508
if (widget) {
@@ -520,20 +528,22 @@ export default {
520528
contextualWidgetDefaultData(type) {
521529
return this.moduleOptions.contextualWidgetDefaultData[type];
522530
},
523-
async insert({ index, widget }) {
531+
async insert({
532+
index, widget, autosave = true
533+
} = {}) {
524534
if (!widget._id) {
525535
widget._id = createId();
526536
}
527537
if (!widget.metaType) {
528538
widget.metaType = 'widget';
529539
}
530-
const push = {
531-
$each: [ widget ]
532-
};
533-
if (index < this.next.length) {
534-
push.$before = this.next[index]._id;
535-
}
536-
if (this.docId === window.apos.adminBar.contextId) {
540+
if (autosave && (this.docId === window.apos.adminBar.contextId)) {
541+
const push = {
542+
$each: [ widget ]
543+
};
544+
if (index < this.next.length) {
545+
push.$before = this.next[index]._id;
546+
}
537547
apos.bus.$emit('context-edited', {
538548
$push: {
539549
[`@${this.id}.items`]: push
@@ -561,6 +571,15 @@ export default {
561571
widgetEditorComponent(type) {
562572
return this.moduleOptions.components.widgetEditors[type];
563573
},
574+
widgetPreview(type, index, create) {
575+
return this.moduleOptions.widgetPreview[type]
576+
? {
577+
area: this,
578+
index,
579+
create
580+
}
581+
: null;
582+
},
564583
// Recursively seek `subObject` within `object`, based on whether
565584
// its _id matches that of a sub-object of `object`. If found,
566585
// replace that sub-object with `subObject` and return `true`.

modules/@apostrophecms/area/views/area.html

-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{# area needs its own copy of the widget options as #}
22
{# JSON, for adding new widgets #}
3-
{%- set isSingleton = data.options.limit == 1 and data.options.type -%}
43

54
<div class="apos-area" {%- if data.canEdit %} data-apos-area-newly-editable data-doc-id='{{ data.area._docId | jsonAttribute({ single: true }) }}' data-field-id='{{ data.field._id | jsonAttribute({ single: true }) }}' data-options='{{ apos.util.omit(data.options, 'area') | jsonAttribute({ single: true }) }}' data='{{ data.area | jsonAttribute({ single: true }) }}' data-choices='{{ data.choices | jsonAttribute({ single: true }) }}'{% endif %}>
65
{%- for item in data.area.items -%}

modules/@apostrophecms/image-widget/views/widget.html

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
{% set attachment = apos.image.first(data.widget._image) %}
2+
13
{% if data.widget.aposPlaceholder and data.manager.options.placeholderUrl %}
24
<img
35
src="{{ data.manager.options.placeholderUrl }}"
@@ -10,7 +12,6 @@
1012
{% set loadingType = data.options.loadingType or data.manager.options.loadingType %}
1113
{% set size = data.options.size or data.manager.options.size or 'full' %}
1214

13-
{% set attachment = apos.image.first(data.widget._image) %}
1415

1516
{% if attachment %}
1617
<img {% if className %} class="{{ className }}"{% endif %}

modules/@apostrophecms/schema/index.js

-4
Original file line numberDiff line numberDiff line change
@@ -684,10 +684,6 @@ module.exports = {
684684
nonVisibleFields
685685
});
686686

687-
for (const error of validErrors) {
688-
self.apos.util.error(error.stack);
689-
}
690-
691687
if (validErrors.length) {
692688
throw validErrors;
693689
}

modules/@apostrophecms/video-widget/ui/src/index.js

+16-27
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export default () => {
2+
apos.oembedCache ||= {};
23
apos.util.widgetPlayers['@apostrophecms/video'] = {
34
selector: '[data-apos-video-widget]',
45
player: function(el) {
@@ -30,15 +31,25 @@ export default () => {
3031
}
3132

3233
function query(options, callback) {
34+
if (Object.hasOwn(apos.oembedCache, options.url)) {
35+
return callback(null, apos.oembedCache[options.url]);
36+
}
3337
const opts = {
3438
qs: {
3539
url: options.url
3640
}
3741
};
38-
return apos.http.get('/api/v1/@apostrophecms/oembed/query', opts, callback);
42+
return apos.http.get('/api/v1/@apostrophecms/oembed/query', opts, function(err, result) {
43+
if (err) {
44+
return callback(err);
45+
}
46+
apos.oembedCache[options.url] = result;
47+
return callback(null, result);
48+
});
3949
}
4050

4151
function play(el, result) {
52+
// Use aspect-ratio to eliminate the need for any timeout at all
4253
const shaker = document.createElement('div');
4354
shaker.innerHTML = result.html;
4455
const inner = shaker.firstChild;
@@ -49,33 +60,11 @@ export default () => {
4960
}
5061
inner.removeAttribute('width');
5162
inner.removeAttribute('height');
52-
el.append(inner);
53-
// wait for CSS width to be known
54-
setTimeout(function() {
55-
// If oembed results include width and height we can get the
56-
// video aspect ratio right
57-
if (result.width && result.height) {
58-
inner.style.width = '100%';
59-
resizeVideo(inner);
60-
// If we need to initially size the video, also resize it on window
61-
// resize.
62-
window.addEventListener('resize', resizeHandler);
63-
} else {
64-
// No, so assume the oembed HTML code is responsive.
65-
}
66-
}, 0);
67-
}
68-
69-
function resizeVideo(canvasEl) {
70-
canvasEl.style.height = ((queryResult.height / queryResult.width) * canvasEl.offsetWidth) + 'px';
71-
};
72-
73-
function resizeHandler() {
74-
if (document.contains(el)) {
75-
resizeVideo(el.querySelector('[data-apos-video-canvas]'));
76-
} else {
77-
window.removeEventListener('resize', resizeHandler);
63+
if (result.width && result.height) {
64+
inner.style.width = '100%';
65+
inner.style.aspectRatio = `${queryResult.width} / ${queryResult.height}`;
7866
}
67+
el.append(inner);
7968
}
8069

8170
function fail(err) {

modules/@apostrophecms/video-widget/views/widget.html

+1-4
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,7 @@
1818
data-apos-video-widget
1919
data-apos-video-url={{ data.widget.video.url }}
2020
>
21-
{% if data.widget.video.thumbnail %}
22-
<img src="{{ data.widget.video.thumbnail }}" alt="{{ data.widget.video.thumbnail }}"/>
23-
{% endif %}
24-
</div>
21+
</div>
2522
{% elif data.user %}
2623
<p {% if data.manager.options.className %} class="{{ data.manager.options.className }} {{ data.manager.options.className }}--error"{% endif %}>No video selected</p>
2724
{% endif %}

modules/@apostrophecms/widget-type/index.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@
5858
// that information will not be available yet in any asynchronous node.js code.
5959
// It is the last thing to happen before the actual page template rendering.
6060
//
61+
// ### `preview`
62+
//
63+
// If true, the image widget is automatically previewed live following changes in the editor modal.
64+
// Should not be combined with `contextual`.
65+
//
6166
// ## Fields
6267
//
6368
// You will need to configure the schema fields for your widget using
@@ -420,7 +425,8 @@ module.exports = {
420425
className: self.options.className,
421426
components: self.options.components,
422427
width: self.options.width,
423-
origin: self.options.origin
428+
origin: self.options.origin,
429+
preview: self.options.preview
424430
});
425431
return result;
426432
}

0 commit comments

Comments
 (0)