diff --git a/CHANGELOG.md b/CHANGELOG.md index 01207d3e31..142367e805 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Adds any alt text found in an attribute to the media library attachment during import of rich text inline images by API * Adds `prependNodes` and `appendNodes` methods to every module. These methods allow you to inject HTML to every page using a `node` declaration. +* Adds quick image upload UI to `@apostrophecms/image-widget`. ### Changes diff --git a/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue b/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue index a5dafa77dc..808bca2e82 100644 --- a/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +++ b/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue @@ -631,7 +631,8 @@ export default { widget: clipboard, index }); - } else if (this.widgetIsContextual(name)) { + } + if (this.widgetIsContextual(name)) { return this.insert({ widget: { type: name, @@ -640,7 +641,8 @@ export default { }, index }); - } else if (!this.widgetHasInitialModal(name)) { + } + if (!this.widgetHasInitialModal(name)) { const widget = this.newWidget(name); return this.insert({ widget: { @@ -649,26 +651,26 @@ export default { }, index }); - } else { - const componentName = this.widgetEditorComponent(name); - apos.area.activeEditor = this; - const preview = this.widgetPreview(name, index, true); - const widget = await apos.modal.execute(componentName, { - modelValue: null, - options: this.widgetOptionsByType(name), - type: name, - docId: this.docId, - areaFieldId: this.fieldId, - parentFollowingValues: this.followingValues, - preview + } + + const componentName = this.widgetEditorComponent(name); + apos.area.activeEditor = this; + const preview = this.widgetPreview(name, index, true); + const widget = await apos.modal.execute(componentName, { + modelValue: null, + options: this.widgetOptionsByType(name), + type: name, + docId: this.docId, + areaFieldId: this.fieldId, + parentFollowingValues: this.followingValues, + preview + }); + apos.area.activeEditor = null; + if (widget) { + return this.insert({ + widget, + index }); - apos.area.activeEditor = null; - if (widget) { - return this.insert({ - widget, - index - }); - } } }, widgetOptionsByType(name) { diff --git a/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue b/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue index 5469b6cc33..ebf06f92bf 100644 --- a/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +++ b/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue @@ -156,6 +156,7 @@ :doc-id="docId" :rendering="rendering" @edit="$emit('edit', i);" + @update="$emit('update', $event);" />
+
+ +
+ +
+ + + + + diff --git a/modules/@apostrophecms/image/ui/apos/components/AposMediaUploader.vue b/modules/@apostrophecms/image/ui/apos/components/AposMediaUploader.vue index eb4fba0641..9ba38df9e4 100644 --- a/modules/@apostrophecms/image/ui/apos/components/AposMediaUploader.vue +++ b/modules/@apostrophecms/image/ui/apos/components/AposMediaUploader.vue @@ -97,13 +97,6 @@ export default { this.dragoverCount--; this.dragover = this.dragoverCount > 0; }, - dragHandler (event) { - if (this.disabled) { - return; - } - event.preventDefault(); - this.dragging = true; - }, async uploadMedia (event) { try { apos.bus.$emit('busy', { diff --git a/modules/@apostrophecms/image/ui/apos/components/AposMediaUploaderUi.vue b/modules/@apostrophecms/image/ui/apos/components/AposMediaUploaderUi.vue new file mode 100644 index 0000000000..0eaacd2212 --- /dev/null +++ b/modules/@apostrophecms/image/ui/apos/components/AposMediaUploaderUi.vue @@ -0,0 +1,386 @@ + + + + + diff --git a/modules/@apostrophecms/widget-type/ui/apos/composables/AposWidget.js b/modules/@apostrophecms/widget-type/ui/apos/composables/AposWidget.js new file mode 100644 index 0000000000..77500acf1e --- /dev/null +++ b/modules/@apostrophecms/widget-type/ui/apos/composables/AposWidget.js @@ -0,0 +1,97 @@ +import { isEqual } from 'lodash'; +import { + ref, unref, computed, nextTick +} from 'vue'; + +// When modifyin this file, verify that `AposWidgetMixin.js` still works +export function useAposWidget(props) { + const rendered = ref('...'); + + const moduleOptions = computed(() => { + return apos.modules[apos.area.widgetManagers[props.type]]; + }); + + return { + rendered, + renderContent, + getClasses: () => _getClasses({ + modelValue: props.modelValue, + moduleOptions + }) + }; + + async function renderContent() { + const result = await _renderContent(props); + if (Object.hasOwn(result, 'data')) { + rendered.value = result.data; + } + if (!result.error) { + nextTick(() => { + _emitWidgetRendered(props.modelValue.aposLivePreview); + }); + } + } +}; + +export function _getClasses(_modelValue, _moduleOptions) { + const moduleOptions = unref(_moduleOptions); + const modelValue = unref(_modelValue); + const { placeholderClass } = moduleOptions; + + if (!placeholderClass) { + return {}; + } + + return { + [placeholderClass]: modelValue.aposPlaceholder === true + }; +} + +export async function _renderContent(props) { + apos.bus.$emit('widget-rendering'); + const { + aposLivePreview, + ...widget + } = props.modelValue; + const parameters = { + _docId: props.docId, + widget, + areaFieldId: props.areaFieldId, + type: props.type + }; + try { + if (props.rendering && (isEqual(props.rendering.parameters, parameters))) { + return { data: props.rendering.html }; + } + // Don't use a placeholder here, it causes flickering in live preview + // mode. It is better to display the old until we display the new, we + // have "busy" for clarity + const result = await apos.http.post( + `${apos.area.action}/render-widget?aposEdit=1&aposMode=${props.mode}`, + { + busy: !aposLivePreview, + body: { + ...parameters, + livePreview: aposLivePreview + } + }); + if (result !== 'aposLivePreviewSchemaNotYetValid') { + return { data: result }; + } + return {}; + } catch (e) { + // eslint-disable-next-line no-console + console.error('Unable to render widget. Possibly the schema has been changed and the existing widget does not pass validation.', e); + return { + data: '

Unable to render this widget.

', + error: true + }; + } +} + +// Wait for reactivity to render v-html so that markup is +// in the DOM before hinting that it might be time to prepare +// sub-area editors and run players (done in mixin and composable) +export function _emitWidgetRendered(aposLivePreview) { + apos.bus.$emit('widget-rendered', { edit: !aposLivePreview }); +} diff --git a/modules/@apostrophecms/widget-type/ui/apos/composables/AposWidgetProps.js b/modules/@apostrophecms/widget-type/ui/apos/composables/AposWidgetProps.js new file mode 100644 index 0000000000..25e705c01c --- /dev/null +++ b/modules/@apostrophecms/widget-type/ui/apos/composables/AposWidgetProps.js @@ -0,0 +1,28 @@ +export default { + // NOTE: docId is always null, investigate if needed + docId: String, + type: String, + areaFieldId: String, + modelValue: Object, + mode: { + type: String, + default: 'draft' + }, + meta: { + type: Object, + default() { + return {}; + } + }, + // Ignored for server side rendering + areaField: Object, + followingValues: Object, + // Fix missing prop rendered as `[object Object]` attribute in the DOM + options: Object, + rendering: { + type: Object, + default() { + return null; + } + } +}; diff --git a/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js b/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js index b2eec602ca..c50af78f58 100644 --- a/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js +++ b/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js @@ -1,33 +1,14 @@ -import { isEqual } from 'lodash'; +import props from '../composables/AposWidgetProps.js'; +import { + _renderContent, _emitWidgetRendered, _getClasses +} from '../composables/AposWidget.js'; export default { - props: { - // NOTE: docId is always null, investigate if needed - docId: String, - type: String, - areaFieldId: String, - modelValue: Object, - mode: { - type: String, - default: 'draft' - }, - meta: { - type: Object, - default() { - return {}; - } - }, - // Ignored for server side rendering - areaField: Object, - followingValues: Object, - // Fix missing prop rendered as `[object Object]` attribute in the DOM - options: Object, - rendering: { - type: Object, - default() { - return null; - } - } + props, + data() { + return { + rendered: '...' + }; }, watch: { modelValue: { @@ -36,73 +17,28 @@ export default { } } }, - data() { - return { - rendered: '...' - }; - }, - mounted() { - this.renderContent(); - }, computed: { moduleOptions() { return apos.modules[apos.area.widgetManagers[this.type]]; } }, + mounted() { + this.renderContent(); + }, methods: { + getClasses() { + return _getClasses(this.modelValue, this.moduleOptions); + }, async renderContent() { - apos.bus.$emit('widget-rendering'); - const { - aposLivePreview, - ...widget - } = this.modelValue; - const parameters = { - _docId: this.docId, - widget, - areaFieldId: this.areaFieldId, - type: this.type - }; - try { - if (this.rendering && (isEqual(this.rendering.parameters, parameters))) { - this.rendered = this.rendering.html; - } else { - // Don't use a placeholder here, it causes flickering in live preview - // mode. It is better to display the old until we display the new, we - // have "busy" for clarity - const result = await apos.http.post(`${apos.area.action}/render-widget?aposEdit=1&aposMode=${this.mode}`, { - busy: !aposLivePreview, - body: { - ...parameters, - livePreview: aposLivePreview - } - }); - // - if (result !== 'aposLivePreviewSchemaNotYetValid') { - this.rendered = result; - } - } - // Wait for reactivity to render v-html so that markup is - // in the DOM before hinting that it might be time to prepare - // sub-area editors and run players - setTimeout(function() { - apos.bus.$emit('widget-rendered', { edit: !aposLivePreview }); - }, 0); - } catch (e) { - this.rendered = '

Unable to render this widget.

'; - // eslint-disable-next-line no-console - console.error('Unable to render widget. Possibly the schema has been changed and the existing widget does not pass validation.', e); + const result = await _renderContent(this.$props); + if (Object.hasOwn(result, 'data')) { + this.rendered = result.data; } - }, - getClasses() { - const { placeholderClass } = this.moduleOptions; - - if (!placeholderClass) { - return {}; + if (!result.error) { + this.$nextTick(() => { + _emitWidgetRendered(this.modelValue.aposLivePreview); + }); } - - return { - [placeholderClass]: this.modelValue.aposPlaceholder === true - }; } } }; diff --git a/test/widgets.js b/test/widgets.js index 8663deac62..c756b8f0fc 100644 --- a/test/widgets.js +++ b/test/widgets.js @@ -273,33 +273,6 @@ describe('Widgets', function() { }); const mediaWidgetTypeToAssertion = { - image: { - placeholderUrlOverride: '/modules/@apostrophecms/my-image-widget/placeholder.webp', - assertAposPlaceholderTrue(document) { - const imgNodes = document.querySelectorAll('img'); - assert(imgNodes.length === 1); - assert(imgNodes[0].classList.contains('image-widget-placeholder')); - assert(imgNodes[0].alt === 'Image placeholder'); - assert(imgNodes[0].src === '/apos-frontend/default/modules/@apostrophecms/image-widget/placeholder.jpg'); - }, - assertPreviewMode(document) { - const imgNodes = document.querySelectorAll('img'); - - assert(imgNodes.length === 0); - }, - assertFalsyPlaceholderUrl(document) { - const imgNodes = document.querySelectorAll('img'); - assert(imgNodes.length === 0); - }, - assertPlaceholderUrlOverride(document) { - const imgNodes = document.querySelectorAll('img'); - - assert(imgNodes.length === 1); - assert(imgNodes[0].classList.contains('image-widget-placeholder')); - assert(imgNodes[0].alt === 'Image placeholder'); - assert(imgNodes[0].src === '/apos-frontend/default/modules/@apostrophecms/my-image-widget/placeholder.webp'); - } - }, video: { placeholderUrlOverride: 'https://vimeo.com/57946935', assertAposPlaceholderTrue(document) {