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/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<div>
|
|
3
3
|
<bubble-menu
|
|
4
4
|
class="bubble-menu"
|
|
5
|
-
:tippy-options="{ duration: 100 }"
|
|
5
|
+
:tippy-options="{ duration: 100, zIndex: 2000 }"
|
|
6
6
|
:editor="editor"
|
|
7
7
|
v-if="editor"
|
|
8
8
|
>
|
|
@@ -25,6 +25,47 @@
|
|
|
25
25
|
</div>
|
|
26
26
|
</AposContextMenuDialog>
|
|
27
27
|
</bubble-menu>
|
|
28
|
+
<floating-menu
|
|
29
|
+
class="apos-rich-text-insert-menu" :should-show="showFloatingMenu"
|
|
30
|
+
:editor="editor" :tippy-options="{ duration: 100, zIndex: 2000 }"
|
|
31
|
+
v-if="editor"
|
|
32
|
+
>
|
|
33
|
+
<div class="apos-rich-text-insert-menu-heading">
|
|
34
|
+
{{ $t('apostrophe:richTextInsertMenuHeading') }}
|
|
35
|
+
</div>
|
|
36
|
+
<div
|
|
37
|
+
v-for="(item, index) in insert"
|
|
38
|
+
:key="`${item}-${index}`"
|
|
39
|
+
class="apos-rich-text-insert-menu-item"
|
|
40
|
+
>
|
|
41
|
+
<div class="apos-rich-text-insert-menu-icon">
|
|
42
|
+
<AposIndicator
|
|
43
|
+
:icon="insertMenu[item].icon"
|
|
44
|
+
:icon-size="35"
|
|
45
|
+
class="apos-button__icon"
|
|
46
|
+
fill-color="currentColor"
|
|
47
|
+
@click="activateInsertMenuItem(item, insertMenu[item])"
|
|
48
|
+
/>
|
|
49
|
+
<component
|
|
50
|
+
v-if="item === activeInsertMenuComponent?.name"
|
|
51
|
+
:is="activeInsertMenuComponent.component"
|
|
52
|
+
:active="true"
|
|
53
|
+
:editor="editor"
|
|
54
|
+
:options="editorOptions"
|
|
55
|
+
@before-commands="removeSlash"
|
|
56
|
+
@close="closeInsertMenuItem"
|
|
57
|
+
@click.stop="$event => null"
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
<div
|
|
61
|
+
class="apos-rich-text-insert-menu-label"
|
|
62
|
+
@click="activateInsertMenuItem(item, insertMenu[item])"
|
|
63
|
+
>
|
|
64
|
+
<h4>{{ $t(insertMenu[item].label) }}</h4>
|
|
65
|
+
<p>{{ $t(insertMenu[item].description) }}</p>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</floating-menu>
|
|
28
69
|
<div class="apos-rich-text-editor__editor" :class="editorModifiers">
|
|
29
70
|
<editor-content :editor="editor" :class="editorOptions.className" />
|
|
30
71
|
</div>
|
|
@@ -41,7 +82,8 @@
|
|
|
41
82
|
import {
|
|
42
83
|
Editor,
|
|
43
84
|
EditorContent,
|
|
44
|
-
BubbleMenu
|
|
85
|
+
BubbleMenu,
|
|
86
|
+
FloatingMenu
|
|
45
87
|
} from '@tiptap/vue-2';
|
|
46
88
|
import StarterKit from '@tiptap/starter-kit';
|
|
47
89
|
import TextAlign from '@tiptap/extension-text-align';
|
|
@@ -59,7 +101,8 @@ export default {
|
|
|
59
101
|
name: 'AposRichTextWidgetEditor',
|
|
60
102
|
components: {
|
|
61
103
|
EditorContent,
|
|
62
|
-
BubbleMenu
|
|
104
|
+
BubbleMenu,
|
|
105
|
+
FloatingMenu
|
|
63
106
|
},
|
|
64
107
|
props: {
|
|
65
108
|
type: {
|
|
@@ -100,7 +143,8 @@ export default {
|
|
|
100
143
|
},
|
|
101
144
|
pending: null,
|
|
102
145
|
isFocused: null,
|
|
103
|
-
showPlaceholder: null
|
|
146
|
+
showPlaceholder: null,
|
|
147
|
+
activeInsertMenuComponent: null
|
|
104
148
|
};
|
|
105
149
|
},
|
|
106
150
|
computed: {
|
|
@@ -150,12 +194,22 @@ export default {
|
|
|
150
194
|
const _class = defaultStyle.class ? ` class="${defaultStyle.class}"` : '';
|
|
151
195
|
return `<${defaultStyle.tag}${_class}></${defaultStyle.tag}>`;
|
|
152
196
|
},
|
|
197
|
+
// Names of active toolbar items for this particular widget, as an array
|
|
153
198
|
toolbar() {
|
|
154
199
|
return this.editorOptions.toolbar;
|
|
155
200
|
},
|
|
201
|
+
// Information about all available toolbar items, as an object
|
|
156
202
|
tools() {
|
|
157
203
|
return this.moduleOptions.tools;
|
|
158
204
|
},
|
|
205
|
+
// Names of active insert menu items for this particular widget, as an array
|
|
206
|
+
insert() {
|
|
207
|
+
return this.editorOptions.insert || [];
|
|
208
|
+
},
|
|
209
|
+
// Information about all available insert menu items, as an object
|
|
210
|
+
insertMenu() {
|
|
211
|
+
return this.moduleOptions.insertMenu;
|
|
212
|
+
},
|
|
159
213
|
isVisuallyEmpty () {
|
|
160
214
|
const div = document.createElement('div');
|
|
161
215
|
div.innerHTML = this.value.content;
|
|
@@ -166,6 +220,12 @@ export default {
|
|
|
166
220
|
if (this.isVisuallyEmpty) {
|
|
167
221
|
classes.push('apos-is-visually-empty');
|
|
168
222
|
}
|
|
223
|
+
// Per Stu's original logic we have to deal with an edge case when the page is
|
|
224
|
+
// first loading by displaying the initial placeholder then too (showPlaceholder
|
|
225
|
+
// state not yet computed)
|
|
226
|
+
if (((this.placeholderText && this.moduleOptions.placeholder) || this.insert.length) && this.isFocused && (this.showPlaceholder !== false)) {
|
|
227
|
+
classes.push('apos-show-initial-placeholder');
|
|
228
|
+
}
|
|
169
229
|
return classes;
|
|
170
230
|
},
|
|
171
231
|
tiptapTextCommands() {
|
|
@@ -175,11 +235,11 @@ export default {
|
|
|
175
235
|
return this.moduleOptions.tiptapTypes;
|
|
176
236
|
},
|
|
177
237
|
placeholderText() {
|
|
178
|
-
return this.moduleOptions.placeholderText;
|
|
238
|
+
return this.insert.length > 0 ? this.moduleOptions.placeholderTextWithInsertMenu : (this.moduleOptions.placeholderText || '');
|
|
179
239
|
}
|
|
180
240
|
},
|
|
181
241
|
watch: {
|
|
182
|
-
|
|
242
|
+
isFocused(newVal) {
|
|
183
243
|
if (!newVal) {
|
|
184
244
|
if (this.pending) {
|
|
185
245
|
this.emitWidgetUpdate();
|
|
@@ -188,6 +248,8 @@ export default {
|
|
|
188
248
|
}
|
|
189
249
|
},
|
|
190
250
|
mounted() {
|
|
251
|
+
// Cleanly namespace it so we don't conflict with other uses and instances
|
|
252
|
+
const CustomPlaceholder = Placeholder.extend();
|
|
191
253
|
const extensions = [
|
|
192
254
|
StarterKit.configure({
|
|
193
255
|
document: false,
|
|
@@ -205,23 +267,14 @@ export default {
|
|
|
205
267
|
TableCell,
|
|
206
268
|
TableHeader,
|
|
207
269
|
TableRow,
|
|
208
|
-
|
|
209
|
-
// since `placeholderText` option is enough to decide whether to display it or not.
|
|
210
|
-
this.placeholderText && Placeholder.configure({
|
|
270
|
+
CustomPlaceholder.configure({
|
|
211
271
|
placeholder: () => {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (this.showPlaceholder === null) {
|
|
219
|
-
return this.$t(this.placeholderText);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return this.showPlaceholder ? this.$t(this.placeholderText) : '';
|
|
223
|
-
}
|
|
224
|
-
})
|
|
272
|
+
const text = this.$t(this.placeholderText);
|
|
273
|
+
return text;
|
|
274
|
+
},
|
|
275
|
+
emptyNodeClass: this.insert.length ? 'apos-is-empty' : 'apos-is-empty-without-insert'
|
|
276
|
+
}),
|
|
277
|
+
FloatingMenu
|
|
225
278
|
]
|
|
226
279
|
.filter(Boolean)
|
|
227
280
|
.concat(this.aposTiptapExtensions());
|
|
@@ -254,12 +307,19 @@ export default {
|
|
|
254
307
|
});
|
|
255
308
|
}
|
|
256
309
|
});
|
|
310
|
+
apos.bus.$on('apos-refreshing', this.onAposRefreshing);
|
|
257
311
|
},
|
|
258
312
|
|
|
259
313
|
beforeDestroy() {
|
|
260
314
|
this.editor.destroy();
|
|
315
|
+
apos.bus.$off('apos-refreshing', this.onAposRefreshing);
|
|
261
316
|
},
|
|
262
317
|
methods: {
|
|
318
|
+
onAposRefreshing(refreshOptions) {
|
|
319
|
+
if (this.activeInsertMenuComponent) {
|
|
320
|
+
refreshOptions.refresh = false;
|
|
321
|
+
}
|
|
322
|
+
},
|
|
263
323
|
async editorUpdate() {
|
|
264
324
|
// Hint that we are typing, even though we're going to
|
|
265
325
|
// debounce the actual updates for performance
|
|
@@ -424,6 +484,58 @@ export default {
|
|
|
424
484
|
styles: this.editorOptions.styles.map(this.localizeStyle),
|
|
425
485
|
types: this.tiptapTypes
|
|
426
486
|
}));
|
|
487
|
+
},
|
|
488
|
+
showFloatingMenu({ state }) {
|
|
489
|
+
if (!this.insertMenu || !this.insert.length) {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
const { $from, $to } = state.selection;
|
|
493
|
+
if (state.selection.empty) {
|
|
494
|
+
if ($to.nodeBefore && $to.nodeBefore.text) {
|
|
495
|
+
const text = $to.nodeBefore.text;
|
|
496
|
+
// Only show when the user has just entered a '/' character or
|
|
497
|
+
// an insert menu component is active
|
|
498
|
+
if (text === '/') {
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return false;
|
|
503
|
+
} else if (state.doc.textBetween($from, $to, ' ') === '/') {
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
return false;
|
|
507
|
+
},
|
|
508
|
+
activateInsertMenuItem(name, info) {
|
|
509
|
+
// Select the / and remove it
|
|
510
|
+
if (info.component) {
|
|
511
|
+
this.activeInsertMenuComponent = {
|
|
512
|
+
name,
|
|
513
|
+
...info
|
|
514
|
+
};
|
|
515
|
+
} else {
|
|
516
|
+
this.removeSlash();
|
|
517
|
+
this.editor.commands[info.action || name]();
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
removeSlash() {
|
|
521
|
+
const state = this.editor.state;
|
|
522
|
+
const { $to } = state.selection;
|
|
523
|
+
if (state.selection.empty && $to?.nodeBefore?.text) {
|
|
524
|
+
const text = $to.nodeBefore.text;
|
|
525
|
+
if (text === '/') {
|
|
526
|
+
const pos = this.editor.view.state.selection.$anchor.pos;
|
|
527
|
+
// Select the slash so an insert operation can replace it
|
|
528
|
+
this.editor.commands.setTextSelection({
|
|
529
|
+
from: pos - 1,
|
|
530
|
+
to: pos
|
|
531
|
+
});
|
|
532
|
+
this.editor.commands.deleteSelection();
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
closeInsertMenuItem() {
|
|
537
|
+
this.removeSlash();
|
|
538
|
+
this.activeInsertMenuComponent = null;
|
|
427
539
|
}
|
|
428
540
|
}
|
|
429
541
|
};
|
|
@@ -472,7 +584,8 @@ function traverseNextNode(node) {
|
|
|
472
584
|
outline: none;
|
|
473
585
|
}
|
|
474
586
|
|
|
475
|
-
.apos-rich-text-editor__editor ::v-deep .ProseMirror p.is-empty
|
|
587
|
+
.apos-rich-text-editor__editor ::v-deep .ProseMirror:focus p.apos-is-empty::before,
|
|
588
|
+
.apos-rich-text-editor__editor.apos-is-visually-empty ::v-deep .ProseMirror:focus p:first-of-type::before {
|
|
476
589
|
content: attr(data-placeholder);
|
|
477
590
|
float: left;
|
|
478
591
|
pointer-events: none;
|
|
@@ -543,4 +656,52 @@ function traverseNextNode(node) {
|
|
|
543
656
|
// Should be visible on any background, light mode or dark mode
|
|
544
657
|
backdrop-filter: invert(0.1);
|
|
545
658
|
}
|
|
659
|
+
|
|
660
|
+
.apos-rich-text-editor__editor ::v-deep figure.ProseMirror-selectednode {
|
|
661
|
+
opacity: 0.5;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
[data-placeholder] {
|
|
665
|
+
display: none;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.apos-rich-text-insert-menu {
|
|
669
|
+
display: flex;
|
|
670
|
+
flex-direction: column;
|
|
671
|
+
cursor: pointer;
|
|
672
|
+
user-select: none;
|
|
673
|
+
gap: 16px;
|
|
674
|
+
padding: 16px;
|
|
675
|
+
border-radius: var(--a-border-radius);
|
|
676
|
+
box-shadow: var(--a-box-shadow);
|
|
677
|
+
background-color: var(--a-background-primary);
|
|
678
|
+
border: 1px solid var(--a-base-8);
|
|
679
|
+
color: var(--a-base-1);
|
|
680
|
+
font-family: var(--a-family-default);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
.apos-rich-text-insert-menu-item {
|
|
684
|
+
display: flex;
|
|
685
|
+
flex-direction: row;
|
|
686
|
+
gap: 16px;
|
|
687
|
+
&:hover {
|
|
688
|
+
color: var(--a-text-primary);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.apos-rich-text-insert-menu-label {
|
|
693
|
+
display: flex;
|
|
694
|
+
flex-direction: column;
|
|
695
|
+
h4, p {
|
|
696
|
+
margin: 4px;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
.apos-rich-text-insert-menu-icon {
|
|
700
|
+
// Positions the popover meaningfully
|
|
701
|
+
position: relative;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
.apos-rich-text-insert-menu-heading {
|
|
705
|
+
color: var(--a-base-5);
|
|
706
|
+
}
|
|
546
707
|
</style>
|
|
@@ -8,59 +8,21 @@
|
|
|
8
8
|
:icon-only="!!tool.icon"
|
|
9
9
|
:icon="tool.icon || false"
|
|
10
10
|
:modifiers="['no-border', 'no-motion']"
|
|
11
|
+
@close="close"
|
|
12
|
+
/>
|
|
13
|
+
<AposImageControlDialog
|
|
14
|
+
:active="active"
|
|
15
|
+
:editor="editor"
|
|
16
|
+
@close="close"
|
|
17
|
+
@click.stop="$event => null"
|
|
11
18
|
/>
|
|
12
|
-
<div
|
|
13
|
-
v-if="active"
|
|
14
|
-
v-click-outside-element="close"
|
|
15
|
-
class="apos-popover apos-image-control__dialog"
|
|
16
|
-
x-placement="bottom"
|
|
17
|
-
:class="{
|
|
18
|
-
'apos-is-triggered': active,
|
|
19
|
-
'apos-has-selection': true
|
|
20
|
-
}"
|
|
21
|
-
>
|
|
22
|
-
<AposContextMenuDialog
|
|
23
|
-
menu-placement="bottom-start"
|
|
24
|
-
>
|
|
25
|
-
<AposSchema
|
|
26
|
-
:schema="schema"
|
|
27
|
-
:trigger-validation="triggerValidation"
|
|
28
|
-
v-model="docFields"
|
|
29
|
-
:utility-rail="false"
|
|
30
|
-
:modifiers="formModifiers"
|
|
31
|
-
:key="lastSelectionTime"
|
|
32
|
-
:generation="generation"
|
|
33
|
-
:following-values="followingValues()"
|
|
34
|
-
:conditional-fields="conditionalFields()"
|
|
35
|
-
/>
|
|
36
|
-
<footer class="apos-image-control__footer">
|
|
37
|
-
<AposButton
|
|
38
|
-
type="default" label="apostrophe:cancel"
|
|
39
|
-
@click="close"
|
|
40
|
-
:modifiers="formModifiers"
|
|
41
|
-
/>
|
|
42
|
-
<AposButton
|
|
43
|
-
type="primary" label="apostrophe:save"
|
|
44
|
-
@click="save"
|
|
45
|
-
:modifiers="formModifiers"
|
|
46
|
-
/>
|
|
47
|
-
</footer>
|
|
48
|
-
</AposContextMenuDialog>
|
|
49
|
-
</div>
|
|
50
19
|
</div>
|
|
51
20
|
</template>
|
|
52
21
|
|
|
53
22
|
<script>
|
|
54
|
-
import AposEditorMixin from 'Modules/@apostrophecms/modal/mixins/AposEditorMixin';
|
|
55
|
-
|
|
56
23
|
export default {
|
|
57
24
|
name: 'AposTiptapImage',
|
|
58
|
-
mixins: [ AposEditorMixin ],
|
|
59
25
|
props: {
|
|
60
|
-
name: {
|
|
61
|
-
type: String,
|
|
62
|
-
required: true
|
|
63
|
-
},
|
|
64
26
|
tool: {
|
|
65
27
|
type: Object,
|
|
66
28
|
required: true
|
|
@@ -72,143 +34,24 @@ export default {
|
|
|
72
34
|
},
|
|
73
35
|
data() {
|
|
74
36
|
return {
|
|
75
|
-
|
|
76
|
-
triggerValidation: false,
|
|
77
|
-
docFields: {
|
|
78
|
-
data: {}
|
|
79
|
-
},
|
|
80
|
-
active: false,
|
|
81
|
-
formModifiers: [ 'small', 'margin-micro' ],
|
|
82
|
-
originalSchema: [
|
|
83
|
-
{
|
|
84
|
-
name: '_image',
|
|
85
|
-
type: 'relationship',
|
|
86
|
-
label: apos.image.label,
|
|
87
|
-
withType: '@apostrophecms/image',
|
|
88
|
-
required: true,
|
|
89
|
-
max: 1,
|
|
90
|
-
// Temporary until we fix our modals to
|
|
91
|
-
// stack interchangeably with tiptap's
|
|
92
|
-
browse: false
|
|
93
|
-
},
|
|
94
|
-
...(getOptions().imageStyles ? [
|
|
95
|
-
{
|
|
96
|
-
name: 'style',
|
|
97
|
-
label: this.$t('apostrophe:style'),
|
|
98
|
-
type: 'select',
|
|
99
|
-
choices: getOptions().imageStyles,
|
|
100
|
-
def: getOptions().imageStyles?.[0].value,
|
|
101
|
-
required: true
|
|
102
|
-
}
|
|
103
|
-
] : []
|
|
104
|
-
),
|
|
105
|
-
{
|
|
106
|
-
name: 'caption',
|
|
107
|
-
label: this.$t('apostrophe:caption'),
|
|
108
|
-
type: 'string'
|
|
109
|
-
}
|
|
110
|
-
]
|
|
37
|
+
active: false
|
|
111
38
|
};
|
|
112
39
|
},
|
|
113
40
|
computed: {
|
|
114
41
|
buttonActive() {
|
|
115
|
-
return this.editor.
|
|
116
|
-
},
|
|
117
|
-
lastSelectionTime() {
|
|
118
|
-
return this.editor.view.lastSelectionTime;
|
|
119
|
-
},
|
|
120
|
-
schema() {
|
|
121
|
-
return this.originalSchema;
|
|
122
|
-
}
|
|
123
|
-
},
|
|
124
|
-
watch: {
|
|
125
|
-
active(newVal) {
|
|
126
|
-
if (newVal) {
|
|
127
|
-
window.addEventListener('keydown', this.keyboardHandler);
|
|
128
|
-
} else {
|
|
129
|
-
window.removeEventListener('keydown', this.keyboardHandler);
|
|
130
|
-
}
|
|
42
|
+
return this.editor.isActive('image');
|
|
131
43
|
}
|
|
132
44
|
},
|
|
133
45
|
methods: {
|
|
134
46
|
click() {
|
|
135
47
|
this.active = !this.active;
|
|
136
|
-
if (this.active) {
|
|
137
|
-
this.populateFields();
|
|
138
|
-
}
|
|
139
48
|
},
|
|
140
49
|
close() {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
this.editor.chain().focus();
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
save() {
|
|
147
|
-
this.triggerValidation = true;
|
|
148
|
-
this.$nextTick(() => {
|
|
149
|
-
if (this.docFields.hasErrors) {
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
const image = this.docFields.data._image[0];
|
|
153
|
-
this.docFields.data.imageId = image && image.aposDocId;
|
|
154
|
-
this.editor.commands.setImage({
|
|
155
|
-
imageId: this.docFields.data.imageId,
|
|
156
|
-
caption: this.docFields.data.caption,
|
|
157
|
-
style: this.docFields.data.style
|
|
158
|
-
});
|
|
159
|
-
this.close();
|
|
160
|
-
});
|
|
161
|
-
},
|
|
162
|
-
keyboardHandler(e) {
|
|
163
|
-
if (e.keyCode === 27) {
|
|
164
|
-
this.close();
|
|
165
|
-
}
|
|
166
|
-
if (e.keyCode === 13) {
|
|
167
|
-
if (this.docFields.data.href || e.metaKey) {
|
|
168
|
-
this.save();
|
|
169
|
-
this.close();
|
|
170
|
-
e.preventDefault();
|
|
171
|
-
} else {
|
|
172
|
-
e.preventDefault();
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
},
|
|
176
|
-
async populateFields() {
|
|
177
|
-
try {
|
|
178
|
-
const attrs = this.editor.getAttributes('image');
|
|
179
|
-
this.docFields.data = {};
|
|
180
|
-
this.schema.forEach((item) => {
|
|
181
|
-
this.docFields.data[item.name] = attrs[item.name] || '';
|
|
182
|
-
});
|
|
183
|
-
const defaultStyle = getOptions().imageStyles?.[0]?.value;
|
|
184
|
-
if (defaultStyle && !this.docFields.data.style) {
|
|
185
|
-
this.docFields.data.style = defaultStyle;
|
|
186
|
-
}
|
|
187
|
-
if (attrs.imageId) {
|
|
188
|
-
try {
|
|
189
|
-
const doc = await apos.http.get(`/api/v1/@apostrophecms/image/${attrs.imageId}`, {
|
|
190
|
-
busy: true
|
|
191
|
-
});
|
|
192
|
-
this.docFields.data._image = [ doc ];
|
|
193
|
-
} catch (e) {
|
|
194
|
-
if (e.status === 404) {
|
|
195
|
-
// No longer available
|
|
196
|
-
this.docFields._image = [];
|
|
197
|
-
} else {
|
|
198
|
-
throw e;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
} finally {
|
|
203
|
-
this.generation++;
|
|
204
|
-
}
|
|
50
|
+
this.active = false;
|
|
51
|
+
this.editor.chain().focus();
|
|
205
52
|
}
|
|
206
53
|
}
|
|
207
54
|
};
|
|
208
|
-
|
|
209
|
-
function getOptions() {
|
|
210
|
-
return apos.modules['@apostrophecms/rich-text-widget'];
|
|
211
|
-
}
|
|
212
55
|
</script>
|
|
213
56
|
|
|
214
57
|
<style lang="scss" scoped>
|
|
@@ -217,45 +60,7 @@ function getOptions() {
|
|
|
217
60
|
display: inline-block;
|
|
218
61
|
}
|
|
219
62
|
|
|
220
|
-
.apos-image-control__dialog {
|
|
221
|
-
z-index: $z-index-modal;
|
|
222
|
-
position: absolute;
|
|
223
|
-
top: calc(100% + 5px);
|
|
224
|
-
left: -15px;
|
|
225
|
-
width: 250px;
|
|
226
|
-
opacity: 0;
|
|
227
|
-
pointer-events: none;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
.apos-image-control__dialog.apos-is-triggered {
|
|
231
|
-
opacity: 1;
|
|
232
|
-
pointer-events: auto;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
63
|
.apos-is-active {
|
|
236
64
|
background-color: var(--a-base-7);
|
|
237
65
|
}
|
|
238
|
-
|
|
239
|
-
.apos-image-control__footer {
|
|
240
|
-
display: flex;
|
|
241
|
-
justify-content: flex-end;
|
|
242
|
-
margin-top: 10px;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
.apos-image-control__footer .apos-button__wrapper {
|
|
246
|
-
margin-left: 7.5px;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
.apos-image-control__remove {
|
|
250
|
-
display: flex;
|
|
251
|
-
justify-content: flex-end;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// special schema style for this use
|
|
255
|
-
.apos-image-control ::v-deep .apos-field--target {
|
|
256
|
-
.apos-field__label {
|
|
257
|
-
display: none;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
66
|
</style>
|
|
@@ -120,7 +120,7 @@ export default {
|
|
|
120
120
|
withType: type,
|
|
121
121
|
required: true,
|
|
122
122
|
max: 1,
|
|
123
|
-
browse:
|
|
123
|
+
browse: true,
|
|
124
124
|
if: {
|
|
125
125
|
linkTo: type
|
|
126
126
|
}
|
|
@@ -307,11 +307,14 @@ function getOptions() {
|
|
|
307
307
|
position: absolute;
|
|
308
308
|
top: calc(100% + 5px);
|
|
309
309
|
left: -15px;
|
|
310
|
-
width: 250px;
|
|
311
310
|
opacity: 0;
|
|
312
311
|
pointer-events: none;
|
|
313
312
|
}
|
|
314
313
|
|
|
314
|
+
.apos-context-menu__dialog {
|
|
315
|
+
width: 500px;
|
|
316
|
+
}
|
|
317
|
+
|
|
315
318
|
.apos-link-control__dialog.apos-is-triggered.apos-has-selection {
|
|
316
319
|
opacity: 1;
|
|
317
320
|
pointer-events: auto;
|
|
@@ -186,9 +186,12 @@ export default {
|
|
|
186
186
|
this.subfields[doc._id] = doc._fields;
|
|
187
187
|
}
|
|
188
188
|
for (const doc of after) {
|
|
189
|
-
if (
|
|
190
|
-
|
|
189
|
+
if (Object.keys(doc._fields || {}).length) {
|
|
190
|
+
continue;
|
|
191
191
|
}
|
|
192
|
+
doc._fields = this.field.schema && (this.subfields[doc._id]
|
|
193
|
+
? this.subfields[doc._id]
|
|
194
|
+
: this.getDefault());
|
|
192
195
|
}
|
|
193
196
|
}
|
|
194
197
|
},
|
|
@@ -317,6 +320,21 @@ export default {
|
|
|
317
320
|
if (this.field.editor === 'AposImageRelationshipEditor') {
|
|
318
321
|
return 'apostrophe:editImageAdjustments';
|
|
319
322
|
}
|
|
323
|
+
},
|
|
324
|
+
getDefault() {
|
|
325
|
+
const object = {};
|
|
326
|
+
this.field.schema.forEach(field => {
|
|
327
|
+
if (field.name.startsWith('_')) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
// Using `hasOwn` here, not simply checking if `field.def` is truthy
|
|
331
|
+
// so that `false`, `null`, `''` or `0` are taken into account:
|
|
332
|
+
const hasDefaultValue = Object.hasOwn(field, 'def');
|
|
333
|
+
object[field.name] = hasDefaultValue
|
|
334
|
+
? klona(field.def)
|
|
335
|
+
: null;
|
|
336
|
+
});
|
|
337
|
+
return object;
|
|
320
338
|
}
|
|
321
339
|
}
|
|
322
340
|
};
|
|
@@ -43,24 +43,6 @@ export default {
|
|
|
43
43
|
return [ this.value.duplicate && 'apos-input--error' ];
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
|
-
async mounted() {
|
|
47
|
-
// Add an null option if there isn't one already
|
|
48
|
-
if (!this.field.required && !this.choices.find(choice => {
|
|
49
|
-
return choice.value === null;
|
|
50
|
-
})) {
|
|
51
|
-
this.choices.unshift({
|
|
52
|
-
label: '',
|
|
53
|
-
value: null
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
this.$nextTick(() => {
|
|
57
|
-
// this has to happen on nextTick to avoid emitting before schemaReady is
|
|
58
|
-
// set in AposSchema
|
|
59
|
-
if (this.field.required && (this.next == null) && (this.choices[0] != null)) {
|
|
60
|
-
this.next = this.choices[0].value;
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
},
|
|
64
46
|
methods: {
|
|
65
47
|
validate(value) {
|
|
66
48
|
if (this.field.required && (value === null)) {
|