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.
- package/CHANGELOG.md +42 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +9 -4
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +19 -1
- package/modules/@apostrophecms/i18n/i18n/en.json +4 -0
- package/modules/@apostrophecms/modal/ui/apos/apps/AposModals.js +33 -0
- package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +1 -0
- package/modules/@apostrophecms/modal/ui/apos/components/TheAposModals.vue +3 -1
- package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +6 -3
- package/modules/@apostrophecms/piece-type/index.js +15 -4
- package/modules/@apostrophecms/rich-text-widget/index.js +30 -5
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue +237 -0
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +184 -23
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapImage.vue +11 -206
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +5 -2
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +1 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +20 -2
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +0 -18
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +37 -18
- package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +15 -1
- package/modules/@apostrophecms/schema/ui/apos/lib/detectChange.js +1 -1
- package/modules/@apostrophecms/schema/ui/apos/mixins/AposInputChoicesMixin.js +21 -0
- package/modules/@apostrophecms/ui/ui/apos/lib/click-outside-element.js +17 -0
- package/modules/@apostrophecms/ui/ui/apos/lib/vue.js +2 -2
- package/modules/@apostrophecms/ui/ui/apos/scss/global/import-all.scss +1 -1
- package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +23 -3
- 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
|
-
|
|
489
|
-
|
|
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 =
|
|
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;
|
|
@@ -252,11 +252,14 @@ export default {
|
|
|
252
252
|
result = false;
|
|
253
253
|
break;
|
|
254
254
|
}
|
|
255
|
-
|
|
256
|
-
|
|
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 !==
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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>
|