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) {