apostrophe 3.44.0 → 3.45.0

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.
Files changed (26) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +9 -4
  3. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +19 -1
  4. package/modules/@apostrophecms/i18n/i18n/en.json +4 -0
  5. package/modules/@apostrophecms/modal/ui/apos/apps/AposModals.js +33 -0
  6. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +1 -0
  7. package/modules/@apostrophecms/modal/ui/apos/components/TheAposModals.vue +3 -1
  8. package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +6 -3
  9. package/modules/@apostrophecms/piece-type/index.js +15 -4
  10. package/modules/@apostrophecms/rich-text-widget/index.js +30 -5
  11. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue +237 -0
  12. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +184 -23
  13. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapImage.vue +11 -206
  14. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +5 -2
  15. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +1 -0
  16. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +20 -2
  17. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +0 -18
  18. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +37 -18
  19. package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +15 -1
  20. package/modules/@apostrophecms/schema/ui/apos/lib/detectChange.js +1 -1
  21. package/modules/@apostrophecms/schema/ui/apos/mixins/AposInputChoicesMixin.js +21 -0
  22. package/modules/@apostrophecms/ui/ui/apos/lib/click-outside-element.js +17 -0
  23. package/modules/@apostrophecms/ui/ui/apos/lib/vue.js +2 -2
  24. package/modules/@apostrophecms/ui/ui/apos/scss/global/import-all.scss +1 -1
  25. package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +23 -3
  26. package/package.json +3 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.45.0 (2023-04-27)
4
+
5
+ ### Adds
6
+
7
+ * Rich text widgets now support the `insert` option, an array
8
+ which currently may contain the strings `image` and `table` in order to add a
9
+ convenient "insert menu" that pops up when the slash key is pressed.
10
+ This provides a better user experience for rich text features that shouldn't
11
+ require that the user select existing text before using them.
12
+ * Auto expand inline array width if needed using `width: max-content` in the admin UI.
13
+ * The "browse" button is now available when selecting pages and pieces
14
+ to link to in the rich text editor.
15
+ * The "browse" button is also available when selecting inline images
16
+ in the rich text editor.
17
+ * Images are now previewed in the relationship field's compact list view.
18
+ * The new `apos-refreshing` Apostrophe bus event can be used to prevent
19
+ Apostrophe from refreshing the main content zone of the page when images
20
+ and pieces are edited, by clearing the `refresh` property of the object
21
+ passed to the event.
22
+ * To facilitate custom click handlers, an `apos.modal.onTopOf(el1, el2)` function is now
23
+ available to check whether an element is considered to be "on top of" another element in
24
+ the modal stack.
25
+
26
+ ### Changes
27
+
28
+ * The `v-click-outside-element` Vue directive now understands that modals "on top of"
29
+ an element should be considered to be "inside" the element, e.g. clicks on them
30
+ shouldn't close the link dialog etc.
31
+
32
+ ### Fixes
33
+
34
+ * Rich text widgets save more reliably when many actions are taken quickly just before save.
35
+
3
36
  ## 3.44.0 (2023-04-13)
4
37
 
5
38
  ### Adds
@@ -13,6 +46,15 @@ This is useful when the goal is to allow REST API access to "guest" users who ha
13
46
  project-specific reasons to fetch access content via REST APIs.
14
47
  * `test-lib/utils.js` has new `createUser` and `loginAs` methods for the convenience of
15
48
  those writing mocha tests of Apostrophe modules.
49
+ * `batchOperations` permissions: if a `permission` property is added to any entry in the `batchOperations` cascade of a piece-type module, this permission will be checked for every user. See `batchOperations` configuration in `modules/@apostrophecms/piece-type/index.js`. The check function `checkBatchOperationsPermissions` can be extended. Please note that this permission is checked only to determine whether to offer the operation.
50
+
51
+ ### Fixes
52
+ * Fix child page slug when title is deleted
53
+
54
+ ### Fixes
55
+
56
+ * Fix various issues on conditional fields that were occurring when adding new widgets with default values or selecting a falsy value in a field that has a conditional field relying on it.
57
+ Populate new or existing doc instances with default values and add an empty `null` choice to select fields that do not have a default value (required or not) and to the ones configured with dynamic choices.
16
58
 
17
59
  ## 3.43.0 (2023-03-29)
18
60
 
@@ -469,7 +469,6 @@ export default {
469
469
  }
470
470
  },
471
471
  async onContentChanged(e) {
472
-
473
472
  if (
474
473
  (e.doc && (e.doc._id === this.context._id)) ||
475
474
  (e.docIds && e.docIds.includes(this.context._id))
@@ -485,9 +484,15 @@ export default {
485
484
  });
486
485
  }
487
486
  }
488
- await this.refresh({
489
- scrollcheck: e.action === 'history'
490
- });
487
+ const refreshOptions = {
488
+ refresh: true
489
+ };
490
+ apos.bus.$emit('apos-refreshing', refreshOptions);
491
+ if (refreshOptions.refresh) {
492
+ await this.refresh({
493
+ scrollcheck: e.action === 'history'
494
+ });
495
+ }
491
496
  },
492
497
  async switchEditMode(editing) {
493
498
  this.editMode = editing;
@@ -464,7 +464,10 @@ export default {
464
464
  this.docType = docData.type;
465
465
  }
466
466
  this.original = klona(docData);
467
- this.docFields.data = docData;
467
+ this.docFields.data = {
468
+ ...this.getDefault(),
469
+ ...docData
470
+ };
468
471
  if (this.published) {
469
472
  this.changed = detectDocChange(this.schema, this.original, this.published, { differences: true });
470
473
  }
@@ -473,6 +476,21 @@ export default {
473
476
  }
474
477
  }
475
478
  },
479
+ getDefault() {
480
+ const doc = {};
481
+ this.schema.forEach(field => {
482
+ if (field.name.startsWith('_')) {
483
+ return;
484
+ }
485
+ // Using `hasOwn` here, not simply checking if `field.def` is truthy
486
+ // so that `false`, `null`, `''` or `0` are taken into account:
487
+ const hasDefaultValue = Object.hasOwn(field, 'def');
488
+ doc[field.name] = hasDefaultValue
489
+ ? klona(field.def)
490
+ : null;
491
+ });
492
+ return doc;
493
+ },
476
494
  async preview() {
477
495
  if (!await this.confirmAndCancel()) {
478
496
  return;
@@ -193,6 +193,7 @@
193
193
  "hideInNavigation": "Hide in Navigation",
194
194
  "home": "Home",
195
195
  "image": "Image",
196
+ "imageDescription": "Add an inline image",
196
197
  "imageFile": "Image File",
197
198
  "imagePlaceholder": "Image placeholder",
198
199
  "imageTag": "Image Tag",
@@ -375,11 +376,13 @@
375
376
  "richTextH4": "Heading 4 (H4)",
376
377
  "richTextHighlight": "Mark",
377
378
  "richTextHorizontalRule": "Horizontal Rule",
379
+ "richTextInsertMenuHeading": "Insert...",
378
380
  "richTextItalic": "Italic",
379
381
  "richTextLink": "Link",
380
382
  "richTextOrderedList": "Ordered List",
381
383
  "richTextParagraph": "Paragraph (P)",
382
384
  "richTextPlaceholder": "Start Typing Here...",
385
+ "richTextPlaceholderWithInsertMenu": "Start Typing Here or Press /...",
383
386
  "richTextRedo": "Redo",
384
387
  "richTextStrikethrough": "Strike",
385
388
  "richTextStyleConfigWarning": "Misconfigured rich text style: label: {{ label }}, {{ tag }}",
@@ -438,6 +441,7 @@
438
441
  "superscript": "Superscript",
439
442
  "switchLocalesAndLocalizePage": "Switch locales and localize page to {{ label }}?",
440
443
  "table": "Table",
444
+ "tableDescription": "Add tabular content to your page",
441
445
  "tagYourImages": "Tag your images to make searching and filtering the media manager easier",
442
446
  "tags": "Tags",
443
447
  "takeActionAndCreateNew": "{{ saveLabel }} and Create New",
@@ -31,6 +31,38 @@ export default function() {
31
31
  },
32
32
  getProperties(id) {
33
33
  return this.$refs.modals.getProperties(id);
34
+ },
35
+ // Returns true if el1 is "on top of" el2 in the
36
+ // modal stack, as viewed by the user. If el1 is a
37
+ // modal that appears later in the stack than el2
38
+ // (visually stacked on top), this method returns true.
39
+ // If el2 is `document` and el1 is a modal, this
40
+ // method returns true. For convenenience, if el1
41
+ // or el2 is not a modal, it is treated as its DOM
42
+ // parent modal, or as `document`. If el1 has no
43
+ // parent modal this method always returns false.
44
+ onTopOf(el1, el2) {
45
+ if (!el1.matches('[data-apos-modal]')) {
46
+ el1 = el1.closest('[data-apos-modal]') || document;
47
+ }
48
+ if (!el2.matches('[data-apos-modal]')) {
49
+ el2 = el2.closest('[data-apos-modal]') || document;
50
+ }
51
+ if (el1 === document) {
52
+ return false;
53
+ }
54
+ if (el2 === document) {
55
+ return true;
56
+ }
57
+ const index1 = apos.modal.stack.findIndex(modal => modal.$el === el1);
58
+ const index2 = apos.modal.stack.findIndex(modal => modal.$el === el2);
59
+ if (index1 === -1) {
60
+ throw new Error('apos.modal.onTopOf: el1 is not in the modal stack');
61
+ }
62
+ if (index2 === -1) {
63
+ throw new Error('apos.modal.onTopOf: el2 is not in the modal stack');
64
+ }
65
+ return index1 > index2;
34
66
  }
35
67
  },
36
68
  render(h) {
@@ -45,6 +77,7 @@ export default function() {
45
77
  apos.modal.execute = theAposModals.execute;
46
78
  apos.modal.getAt = theAposModals.getAt;
47
79
  apos.modal.getProperties = theAposModals.getProperties;
80
+ apos.modal.onTopOf = theAposModals.onTopOf;
48
81
  apos.modal.stack = [];
49
82
  apos.confirm = theAposModals.confirm;
50
83
  apos.alert = theAposModals.alert;
@@ -12,6 +12,7 @@
12
12
  aria-modal="true"
13
13
  :aria-labelledby="id"
14
14
  ref="modalEl"
15
+ data-apos-modal
15
16
  >
16
17
  <transition :name="transitionType">
17
18
  <div
@@ -1,5 +1,7 @@
1
1
  <template>
2
- <div id="apos-modals" :class="themeClass">
2
+ <div
3
+ id="apos-modals" :class="themeClass"
4
+ >
3
5
  <component
4
6
  v-for="modal in stack" :key="modal.id"
5
7
  :is="modal.componentName"
@@ -252,11 +252,14 @@ export default {
252
252
  result = false;
253
253
  break;
254
254
  }
255
- if (Array.isArray(self.getFieldValue(key))) {
256
- result = self.getFieldValue(key).includes(val);
255
+
256
+ const fieldValue = self.getFieldValue(key);
257
+
258
+ if (Array.isArray(fieldValue)) {
259
+ result = fieldValue.includes(val);
257
260
  break;
258
261
  }
259
- if (val !== self.getFieldValue(key)) {
262
+ if (val !== fieldValue) {
260
263
  result = false;
261
264
  break;
262
265
  }
@@ -133,7 +133,8 @@ module.exports = {
133
133
  title: 'apostrophe:publishType',
134
134
  description: 'apostrophe:publishingBatchConfirmation',
135
135
  confirmationButton: 'apostrophe:publishingBatchConfirmationButton'
136
- }
136
+ },
137
+ permission: 'edit'
137
138
  },
138
139
  archive: {
139
140
  label: 'apostrophe:archive',
@@ -149,7 +150,8 @@ module.exports = {
149
150
  title: 'apostrophe:archiveType',
150
151
  description: 'apostrophe:archivingBatchConfirmation',
151
152
  confirmationButton: 'apostrophe:archivingBatchConfirmationButton'
152
- }
153
+ },
154
+ permission: 'edit'
153
155
  },
154
156
  restore: {
155
157
  label: 'apostrophe:restore',
@@ -165,7 +167,8 @@ module.exports = {
165
167
  title: 'apostrophe:restoreType',
166
168
  description: 'apostrophe:restoreBatchConfirmation',
167
169
  confirmationButton: 'apostrophe:restoreBatchConfirmationButton'
168
- }
170
+ },
171
+ permission: 'edit'
169
172
  }
170
173
  },
171
174
  group: {
@@ -1053,6 +1056,14 @@ module.exports = {
1053
1056
  await self.apos.doc.db.deleteMany({ _id: { $in: deletes } });
1054
1057
  deletes.splice(0);
1055
1058
  }
1059
+ },
1060
+ checkBatchOperationsPermissions(req) {
1061
+ return self.batchOperations.filter(batchOperation => {
1062
+ if (batchOperation.permission) {
1063
+ return self.apos.permission.can(req, batchOperation.permission, self.name);
1064
+ }
1065
+ return true;
1066
+ });
1056
1067
  }
1057
1068
  };
1058
1069
  },
@@ -1063,7 +1074,7 @@ module.exports = {
1063
1074
  // Options specific to pieces and their manage modal
1064
1075
  browserOptions.filters = self.filters;
1065
1076
  browserOptions.columns = self.columns;
1066
- browserOptions.batchOperations = self.batchOperations;
1077
+ browserOptions.batchOperations = self.checkBatchOperationsPermissions(req);
1067
1078
  browserOptions.utilityOperations = self.utilityOperations;
1068
1079
  browserOptions.insertViaUpload = self.options.insertViaUpload;
1069
1080
  browserOptions.quickCreate = !self.options.singleton && self.options.quickCreate && self.apos.permission.can(req, 'edit', self.name, 'draft');
@@ -12,6 +12,7 @@ module.exports = {
12
12
  contextual: true,
13
13
  placeholder: true,
14
14
  placeholderText: 'apostrophe:richTextPlaceholder',
15
+ placeholderTextWithInsertMenu: 'apostrophe:richTextPlaceholderWithInsertMenu',
15
16
  defaultData: { content: '' },
16
17
  className: false,
17
18
  linkWithType: [ '@apostrophecms/any-page-type' ],
@@ -203,6 +204,20 @@ module.exports = {
203
204
  icon: 'image-icon'
204
205
  }
205
206
  },
207
+ editorInsertMenu: {
208
+ table: {
209
+ icon: 'table-icon',
210
+ label: 'apostrophe:table',
211
+ action: 'insertTable',
212
+ description: 'apostrophe:tableDescription'
213
+ },
214
+ image: {
215
+ icon: 'image-icon',
216
+ label: 'apostrophe:image',
217
+ description: 'apostrophe:imageDescription',
218
+ component: 'AposImageControlDialog'
219
+ }
220
+ },
206
221
  // Additional properties used in executing tiptap commands
207
222
  // Will be mixed in automatically for developers
208
223
  tiptapTextCommands: {
@@ -243,7 +258,8 @@ module.exports = {
243
258
  },
244
259
  icons: {
245
260
  'format-text-icon': 'FormatText',
246
- 'format-color-highlight-icon': 'FormatColorHighlight'
261
+ 'format-color-highlight-icon': 'FormatColorHighlight',
262
+ 'table-icon': 'Table'
247
263
  },
248
264
  methods(self) {
249
265
  return {
@@ -355,7 +371,7 @@ module.exports = {
355
371
  image: [ 'figure', 'img', 'figcaption' ],
356
372
  div: [ 'div' ]
357
373
  };
358
- for (const item of options.toolbar || []) {
374
+ for (const item of self.combinedItems(options)) {
359
375
  if (simple[item]) {
360
376
  for (const tag of simple[item]) {
361
377
  allowedTags[tag] = true;
@@ -423,7 +439,7 @@ module.exports = {
423
439
  }
424
440
  ]
425
441
  };
426
- for (const item of options.toolbar || []) {
442
+ for (const item of self.combinedItems(options)) {
427
443
  if (simple[item]) {
428
444
  const entries = Array.isArray(simple[item]) ? simple[item] : [ simple[item] ];
429
445
  for (const entry of entries) {
@@ -466,7 +482,7 @@ module.exports = {
466
482
  }
467
483
  }
468
484
  };
469
- for (const item of options.toolbar || []) {
485
+ for (const item of self.combinedItems(options)) {
470
486
  if (simple[item]) {
471
487
  if (!allowedStyles[simple[item].selector]) {
472
488
  allowedStyles[simple[item].selector] = {};
@@ -487,7 +503,7 @@ module.exports = {
487
503
 
488
504
  toolbarToAllowedClasses(options) {
489
505
  const allowedClasses = {};
490
- if ((options.toolbar || []).includes('styles')) {
506
+ if (self.combinedItems(options).includes('styles')) {
491
507
  for (const style of options.styles || []) {
492
508
  const tag = style.tag;
493
509
  const classes = self.getStyleClasses(style);
@@ -503,6 +519,12 @@ module.exports = {
503
519
  return allowedClasses;
504
520
  },
505
521
 
522
+ // Returns a combined array of toolbar and insert menu items from the given
523
+ // set of rich text widget options
524
+ combinedItems(options) {
525
+ return [ ...(options.toolbar || []), ...(options.insert || []) ];
526
+ },
527
+
506
528
  getStyleClasses(heading) {
507
529
  if (!heading.class) {
508
530
  return [];
@@ -700,10 +722,13 @@ module.exports = {
700
722
  const finalData = {
701
723
  ...initialData,
702
724
  tools: self.options.editorTools,
725
+ insertMenu: self.options.editorInsertMenu,
703
726
  defaultOptions: self.options.defaultOptions,
704
727
  tiptapTextCommands: self.options.tiptapTextCommands,
705
728
  tiptapTypes: self.options.tiptapTypes,
706
729
  placeholderText: self.options.placeholder && self.options.placeholderText,
730
+ // Not optional in presence of an insert menu, it's not acceptable UX without it
731
+ placeholderTextWithInsertMenu: self.options.placeholderTextWithInsertMenu,
707
732
  linkWithType: Array.isArray(self.options.linkWithType) ? self.options.linkWithType : [ self.options.linkWithType ],
708
733
  imageStyles: self.options.imageStyles
709
734
  };
@@ -0,0 +1,237 @@
1
+ <template>
2
+ <div
3
+ v-if="active"
4
+ v-click-outside-element="close"
5
+ class="apos-popover apos-image-control__dialog"
6
+ x-placement="bottom"
7
+ :class="{
8
+ 'apos-is-triggered': active,
9
+ 'apos-has-selection': true
10
+ }"
11
+ >
12
+ <AposContextMenuDialog
13
+ menu-placement="bottom-start"
14
+ >
15
+ <AposSchema
16
+ :schema="schema"
17
+ :trigger-validation="triggerValidation"
18
+ v-model="docFields"
19
+ :utility-rail="false"
20
+ :modifiers="formModifiers"
21
+ :key="lastSelectionTime"
22
+ :generation="generation"
23
+ :following-values="followingValues()"
24
+ :conditional-fields="conditionalFields()"
25
+ />
26
+ <footer class="apos-image-control__footer">
27
+ <AposButton
28
+ type="default" label="apostrophe:cancel"
29
+ @click="close"
30
+ :modifiers="formModifiers"
31
+ />
32
+ <AposButton
33
+ type="primary" label="apostrophe:save"
34
+ @click="save"
35
+ :modifiers="formModifiers"
36
+ />
37
+ </footer>
38
+ </AposContextMenuDialog>
39
+ </div>
40
+ </template>
41
+
42
+ <script>
43
+ import AposEditorMixin from 'Modules/@apostrophecms/modal/mixins/AposEditorMixin';
44
+
45
+ export default {
46
+ name: 'AposImageControlDialog',
47
+ mixins: [ AposEditorMixin ],
48
+ props: {
49
+ editor: {
50
+ type: Object,
51
+ required: true
52
+ },
53
+ active: {
54
+ type: Boolean,
55
+ required: true
56
+ }
57
+ },
58
+ emits: [ 'before-commands', 'close' ],
59
+ data() {
60
+ return {
61
+ generation: 1,
62
+ triggerValidation: false,
63
+ docFields: {
64
+ data: {}
65
+ },
66
+ formModifiers: [ 'small', 'margin-micro' ],
67
+ originalSchema: [
68
+ {
69
+ name: '_image',
70
+ type: 'relationship',
71
+ label: apos.image.label,
72
+ withType: '@apostrophecms/image',
73
+ required: true,
74
+ max: 1,
75
+ // Temporary until we fix our modals to
76
+ // stack interchangeably with tiptap's
77
+ browse: true
78
+ },
79
+ ...(getOptions().imageStyles ? [
80
+ {
81
+ name: 'style',
82
+ label: this.$t('apostrophe:style'),
83
+ type: 'select',
84
+ choices: getOptions().imageStyles,
85
+ def: getOptions().imageStyles?.[0].value,
86
+ required: true
87
+ }
88
+ ] : []
89
+ ),
90
+ {
91
+ name: 'caption',
92
+ label: this.$t('apostrophe:caption'),
93
+ type: 'string'
94
+ }
95
+ ]
96
+ };
97
+ },
98
+ computed: {
99
+ lastSelectionTime() {
100
+ return this.editor.view.lastSelectionTime;
101
+ },
102
+ schema() {
103
+ return this.originalSchema;
104
+ }
105
+ },
106
+ watch: {
107
+ active(newVal) {
108
+ if (newVal) {
109
+ window.addEventListener('keydown', this.keyboardHandler);
110
+ this.populateFields();
111
+ } else {
112
+ window.removeEventListener('keydown', this.keyboardHandler);
113
+ }
114
+ }
115
+ },
116
+ methods: {
117
+ close() {
118
+ this.$emit('close');
119
+ },
120
+ save() {
121
+ this.triggerValidation = true;
122
+ this.$nextTick(() => {
123
+ if (this.docFields.hasErrors) {
124
+ return;
125
+ }
126
+ const image = this.docFields.data._image[0];
127
+ this.docFields.data.imageId = image && image.aposDocId;
128
+ this.$emit('before-commands');
129
+ this.editor.commands.setImage({
130
+ imageId: this.docFields.data.imageId,
131
+ caption: this.docFields.data.caption,
132
+ style: this.docFields.data.style
133
+ });
134
+ this.close();
135
+ });
136
+ },
137
+ keyboardHandler(e) {
138
+ if (e.keyCode === 27) {
139
+ this.close();
140
+ }
141
+ if (e.keyCode === 13) {
142
+ if (this.docFields.data.href || e.metaKey) {
143
+ this.save();
144
+ this.close();
145
+ }
146
+ e.preventDefault();
147
+ }
148
+ },
149
+ async populateFields() {
150
+ try {
151
+ const attrs = this.editor.getAttributes('image');
152
+ this.docFields.data = {};
153
+ this.schema.forEach((item) => {
154
+ this.docFields.data[item.name] = attrs[item.name] || '';
155
+ });
156
+ const defaultStyle = getOptions().imageStyles?.[0]?.value;
157
+ if (defaultStyle && !this.docFields.data.style) {
158
+ this.docFields.data.style = defaultStyle;
159
+ }
160
+ if (attrs.imageId) {
161
+ try {
162
+ const doc = await apos.http.get(`/api/v1/@apostrophecms/image/${attrs.imageId}`, {
163
+ busy: true
164
+ });
165
+ this.docFields.data._image = [ doc ];
166
+ } catch (e) {
167
+ if (e.status === 404) {
168
+ // No longer available
169
+ this.docFields._image = [];
170
+ } else {
171
+ throw e;
172
+ }
173
+ }
174
+ }
175
+ } finally {
176
+ this.generation++;
177
+ }
178
+ }
179
+ }
180
+ };
181
+
182
+ function getOptions() {
183
+ return apos.modules['@apostrophecms/rich-text-widget'];
184
+ }
185
+ </script>
186
+
187
+ <style lang="scss" scoped>
188
+ .apos-image-control {
189
+ position: relative;
190
+ display: inline-block;
191
+ }
192
+
193
+ .apos-image-control__dialog {
194
+ z-index: $z-index-modal;
195
+ position: absolute;
196
+ top: calc(100% + 5px);
197
+ left: -15px;
198
+ opacity: 0;
199
+ pointer-events: none;
200
+ }
201
+
202
+ .apos-context-menu__dialog {
203
+ width: 500px;
204
+ }
205
+
206
+ .apos-image-control__dialog.apos-is-triggered {
207
+ opacity: 1;
208
+ pointer-events: auto;
209
+ }
210
+
211
+ .apos-is-active {
212
+ background-color: var(--a-base-7);
213
+ }
214
+
215
+ .apos-image-control__footer {
216
+ display: flex;
217
+ justify-content: flex-end;
218
+ margin-top: 10px;
219
+ }
220
+
221
+ .apos-image-control__footer .apos-button__wrapper {
222
+ margin-left: 7.5px;
223
+ }
224
+
225
+ .apos-image-control__remove {
226
+ display: flex;
227
+ justify-content: flex-end;
228
+ }
229
+
230
+ // special schema style for this use
231
+ .apos-image-control ::v-deep .apos-field--target {
232
+ .apos-field__label {
233
+ display: none;
234
+ }
235
+ }
236
+
237
+ </style>