apostrophe 3.48.0 → 3.50.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 +55 -2
- package/index.js +20 -2
- package/lib/locales.js +1 -1
- package/lib/moog-require.js +3 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +12 -2
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +2 -0
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +7 -24
- package/modules/@apostrophecms/asset/index.js +27 -2
- package/modules/@apostrophecms/asset/lib/webpack/apos/webpack.config.js +23 -2
- package/modules/@apostrophecms/asset/lib/webpack/src/webpack.config.js +26 -2
- package/modules/@apostrophecms/doc/index.js +149 -0
- package/modules/@apostrophecms/doc-type/index.js +9 -1
- package/modules/@apostrophecms/global/index.js +4 -15
- package/modules/@apostrophecms/i18n/i18n/en.json +3 -2
- package/modules/@apostrophecms/i18n/index.js +76 -61
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerDisplay.vue +14 -1
- package/modules/@apostrophecms/login/ui/apos/components/AposForgotPasswordForm.vue +3 -60
- package/modules/@apostrophecms/login/ui/apos/components/AposLoginForm.vue +3 -231
- package/modules/@apostrophecms/login/ui/apos/components/AposResetPasswordForm.vue +3 -96
- package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +2 -99
- package/modules/@apostrophecms/login/ui/apos/logic/AposForgotPasswordForm.js +68 -0
- package/modules/@apostrophecms/login/ui/apos/logic/AposLoginForm.js +239 -0
- package/modules/@apostrophecms/login/ui/apos/logic/AposResetPasswordForm.js +105 -0
- package/modules/@apostrophecms/login/ui/apos/logic/TheAposLogin.js +107 -0
- package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +9 -3
- package/modules/@apostrophecms/modal/ui/apos/components/AposModalToolbar.vue +1 -0
- package/modules/@apostrophecms/page/index.js +124 -1
- package/modules/@apostrophecms/piece-type/index.js +57 -9
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue +11 -8
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +226 -72
- package/modules/@apostrophecms/schema/index.js +0 -1
- package/modules/@apostrophecms/schema/lib/addFieldTypes.js +35 -7
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +21 -1
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +12 -7
- package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +1 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposCombo.vue +178 -20
- package/modules/@apostrophecms/ui/ui/apos/components/AposFilterMenu.vue +1 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposPager.vue +4 -6
- package/modules/@apostrophecms/ui/ui/apos/scss/mixins/_theme_mixins.scss +1 -0
- package/modules/@apostrophecms/util/index.js +5 -6
- package/modules/@apostrophecms/util/ui/src/http.js +6 -3
- package/package.json +20 -3
- package/test/change-doc-ids.js +134 -0
- package/test/i18n.js +310 -0
- package/test/static-i18n.js +0 -105
package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
2
|
+
<div :aria-controls="`insert-menu-${value._id}`" @keydown="handleUIKeydown">
|
|
3
3
|
<bubble-menu
|
|
4
4
|
class="bubble-menu"
|
|
5
5
|
:tippy-options="{ maxWidth: 'none', duration: 100, zIndex: 2000 }"
|
|
@@ -26,44 +26,64 @@
|
|
|
26
26
|
</AposContextMenuDialog>
|
|
27
27
|
</bubble-menu>
|
|
28
28
|
<floating-menu
|
|
29
|
-
class="apos-rich-text-insert-menu" :should-show="showFloatingMenu"
|
|
30
|
-
:editor="editor"
|
|
31
|
-
:tippy-options="{ duration: 100, zIndex: 2000 }"
|
|
32
29
|
v-if="editor"
|
|
30
|
+
class="apos-rich-text-insert-menu"
|
|
31
|
+
:tippy-options="{ duration: 100, zIndex: 2000, placement: 'bottom-start' }"
|
|
32
|
+
:should-show="showFloatingMenu"
|
|
33
|
+
:editor="editor"
|
|
34
|
+
role="listbox"
|
|
35
|
+
tabindex="0"
|
|
36
|
+
ref="insertMenu"
|
|
37
|
+
:id="`insert-menu-${value._id}`"
|
|
38
|
+
:key="insertMenuKey"
|
|
33
39
|
>
|
|
34
40
|
<div class="apos-rich-text-insert-menu-heading">
|
|
35
41
|
{{ $t('apostrophe:richTextInsertMenuHeading') }}
|
|
36
42
|
</div>
|
|
37
43
|
<div
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
class="apos-rich-text-insert-menu-wrapper"
|
|
45
|
+
@keydown.prevent.arrow-up="focusInsertMenuItem(true)"
|
|
46
|
+
@keydown.prevent.arrow-down="focusInsertMenuItem()"
|
|
47
|
+
@keydown="closeInsertMenu"
|
|
41
48
|
>
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@click="activateInsertMenuItem(item, insertMenu[item])"
|
|
49
|
-
/>
|
|
50
|
-
<component
|
|
51
|
-
v-if="item === activeInsertMenuComponent?.name"
|
|
52
|
-
:is="activeInsertMenuComponent.component"
|
|
53
|
-
:active="true"
|
|
54
|
-
:editor="editor"
|
|
55
|
-
:options="editorOptions"
|
|
56
|
-
@before-commands="removeSlash"
|
|
57
|
-
@close="closeInsertMenuItem"
|
|
58
|
-
@click.stop="$event => null"
|
|
59
|
-
/>
|
|
60
|
-
</div>
|
|
61
|
-
<div
|
|
62
|
-
class="apos-rich-text-insert-menu-label"
|
|
49
|
+
<button
|
|
50
|
+
v-for="(item, index) in insert"
|
|
51
|
+
:key="`${item}-${index}`"
|
|
52
|
+
class="apos-rich-text-insert-menu-item"
|
|
53
|
+
role="option"
|
|
54
|
+
data-insert-menu-item
|
|
63
55
|
@click="activateInsertMenuItem(item, insertMenu[item])"
|
|
64
56
|
>
|
|
65
|
-
<
|
|
66
|
-
|
|
57
|
+
<div class="apos-rich-text-insert-menu-icon">
|
|
58
|
+
<AposIndicator
|
|
59
|
+
:icon="insertMenu[item].icon"
|
|
60
|
+
:icon-size="24"
|
|
61
|
+
class="apos-button__icon"
|
|
62
|
+
fill-color="currentColor"
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="apos-rich-text-insert-menu-label">
|
|
66
|
+
<h4>{{ $t(insertMenu[item].label) }}</h4>
|
|
67
|
+
<p>{{ $t(insertMenu[item].description) }}</p>
|
|
68
|
+
</div>
|
|
69
|
+
</button>
|
|
70
|
+
<div class="apos-rich-text-insert-menu-components">
|
|
71
|
+
<div
|
|
72
|
+
v-for="(item, index) in insert"
|
|
73
|
+
:key="`${item}-${index}-component`"
|
|
74
|
+
>
|
|
75
|
+
<component
|
|
76
|
+
v-if="item === activeInsertMenuComponent?.name"
|
|
77
|
+
:is="activeInsertMenuComponent.component"
|
|
78
|
+
:active="true"
|
|
79
|
+
:editor="editor"
|
|
80
|
+
:options="editorOptions"
|
|
81
|
+
@before-commands="removeSlash"
|
|
82
|
+
@cancel="cancelInsertMenuItem"
|
|
83
|
+
@done="closeInsertMenuItem"
|
|
84
|
+
@close="closeInsertMenuItem"
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
67
87
|
</div>
|
|
68
88
|
</div>
|
|
69
89
|
</floating-menu>
|
|
@@ -86,7 +106,23 @@ import {
|
|
|
86
106
|
BubbleMenu,
|
|
87
107
|
FloatingMenu
|
|
88
108
|
} from '@tiptap/vue-2';
|
|
89
|
-
|
|
109
|
+
// Starter Kit extensions
|
|
110
|
+
import BlockQuote from '@tiptap/extension-blockquote';
|
|
111
|
+
import Bold from '@tiptap/extension-bold';
|
|
112
|
+
import BulletList from '@tiptap/extension-bullet-list';
|
|
113
|
+
import Code from '@tiptap/extension-code';
|
|
114
|
+
import CodeBlock from '@tiptap/extension-code-block';
|
|
115
|
+
import Dropcursor from '@tiptap/extension-dropcursor';
|
|
116
|
+
import Gapcursor from '@tiptap/extension-gapcursor';
|
|
117
|
+
import HardBreak from '@tiptap/extension-hard-break';
|
|
118
|
+
import History from '@tiptap/extension-history';
|
|
119
|
+
import HorizontalRule from '@tiptap/extension-horizontal-rule';
|
|
120
|
+
import Italic from '@tiptap/extension-italic';
|
|
121
|
+
import OrderedList from '@tiptap/extension-ordered-list';
|
|
122
|
+
import Paragraph from '@tiptap/extension-paragraph';
|
|
123
|
+
import Strike from '@tiptap/extension-strike';
|
|
124
|
+
import Text from '@tiptap/extension-text';
|
|
125
|
+
// End starter kit extensions
|
|
90
126
|
import TextAlign from '@tiptap/extension-text-align';
|
|
91
127
|
import Highlight from '@tiptap/extension-highlight';
|
|
92
128
|
import Underline from '@tiptap/extension-underline';
|
|
@@ -144,8 +180,11 @@ export default {
|
|
|
144
180
|
},
|
|
145
181
|
pending: null,
|
|
146
182
|
isFocused: null,
|
|
183
|
+
isShowingInsert: false,
|
|
147
184
|
showPlaceholder: null,
|
|
148
|
-
activeInsertMenuComponent: null
|
|
185
|
+
activeInsertMenuComponent: null,
|
|
186
|
+
suppressInsertMenu: false,
|
|
187
|
+
insertMenuKey: null
|
|
149
188
|
};
|
|
150
189
|
},
|
|
151
190
|
computed: {
|
|
@@ -246,17 +285,33 @@ export default {
|
|
|
246
285
|
this.emitWidgetUpdate();
|
|
247
286
|
}
|
|
248
287
|
}
|
|
288
|
+
},
|
|
289
|
+
isShowingInsert(newVal) {
|
|
290
|
+
if (newVal) {
|
|
291
|
+
this.focusInsertMenuItem(false, 0);
|
|
292
|
+
}
|
|
249
293
|
}
|
|
250
294
|
},
|
|
251
295
|
mounted() {
|
|
296
|
+
this.insertMenuKey = this.generateKey();
|
|
252
297
|
// Cleanly namespace it so we don't conflict with other uses and instances
|
|
253
298
|
const CustomPlaceholder = Placeholder.extend();
|
|
254
299
|
const extensions = [
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
300
|
+
BlockQuote,
|
|
301
|
+
Bold,
|
|
302
|
+
BulletList,
|
|
303
|
+
Code,
|
|
304
|
+
CodeBlock,
|
|
305
|
+
Dropcursor,
|
|
306
|
+
Gapcursor,
|
|
307
|
+
HardBreak,
|
|
308
|
+
History,
|
|
309
|
+
HorizontalRule,
|
|
310
|
+
Italic,
|
|
311
|
+
OrderedList,
|
|
312
|
+
Paragraph,
|
|
313
|
+
Strike,
|
|
314
|
+
Text,
|
|
260
315
|
TextAlign.configure({
|
|
261
316
|
types: [ 'heading', 'paragraph', 'defaultNode' ]
|
|
262
317
|
}),
|
|
@@ -273,7 +328,7 @@ export default {
|
|
|
273
328
|
const text = this.$t(this.placeholderText);
|
|
274
329
|
return text;
|
|
275
330
|
},
|
|
276
|
-
emptyNodeClass:
|
|
331
|
+
emptyNodeClass: 'apos-is-empty'
|
|
277
332
|
}),
|
|
278
333
|
FloatingMenu
|
|
279
334
|
]
|
|
@@ -316,6 +371,22 @@ export default {
|
|
|
316
371
|
apos.bus.$off('apos-refreshing', this.onAposRefreshing);
|
|
317
372
|
},
|
|
318
373
|
methods: {
|
|
374
|
+
generateKey() {
|
|
375
|
+
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
376
|
+
},
|
|
377
|
+
handleUIKeydown(e) {
|
|
378
|
+
if (e.key === 'Escape') {
|
|
379
|
+
this.doSuppressInsertMenu();
|
|
380
|
+
} else {
|
|
381
|
+
this.suppressInsertMenu = false;
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
doSuppressInsertMenu() {
|
|
385
|
+
this.suppressInsertMenu = true;
|
|
386
|
+
this.activeInsertMenuComponent = null;
|
|
387
|
+
this.insertMenuKey = this.generateKey();
|
|
388
|
+
this.editor.commands.focus();
|
|
389
|
+
},
|
|
319
390
|
onAposRefreshing(refreshOptions) {
|
|
320
391
|
if (this.activeInsertMenuComponent) {
|
|
321
392
|
refreshOptions.refresh = false;
|
|
@@ -335,6 +406,7 @@ export default {
|
|
|
335
406
|
// least once per second if the user is actively typing
|
|
336
407
|
return;
|
|
337
408
|
}
|
|
409
|
+
|
|
338
410
|
this.pending = setTimeout(() => {
|
|
339
411
|
this.emitWidgetUpdate();
|
|
340
412
|
}, 1000);
|
|
@@ -472,24 +544,34 @@ export default {
|
|
|
472
544
|
types: this.tiptapTypes
|
|
473
545
|
}));
|
|
474
546
|
},
|
|
475
|
-
showFloatingMenu({
|
|
476
|
-
|
|
547
|
+
showFloatingMenu({
|
|
548
|
+
state, oldState
|
|
549
|
+
}) {
|
|
550
|
+
const hasChanges = JSON.stringify(state?.doc.toJSON()) !== JSON.stringify(oldState?.doc.toJSON());
|
|
551
|
+
const { $to } = state.selection;
|
|
552
|
+
|
|
553
|
+
if (
|
|
554
|
+
!this.insertMenu ||
|
|
555
|
+
!this.insert.length ||
|
|
556
|
+
!hasChanges ||
|
|
557
|
+
($to.nodeAfter && $to.nodeAfter.text) ||
|
|
558
|
+
this.suppressInsertMenu
|
|
559
|
+
) {
|
|
560
|
+
this.isShowingInsert = false;
|
|
477
561
|
return false;
|
|
478
562
|
}
|
|
479
|
-
|
|
563
|
+
|
|
480
564
|
if (state.selection.empty) {
|
|
481
565
|
if ($to.nodeBefore && $to.nodeBefore.text) {
|
|
482
566
|
const text = $to.nodeBefore.text;
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
if (text === '/') {
|
|
567
|
+
if (text.slice(-1) === '/') {
|
|
568
|
+
this.isShowingInsert = true;
|
|
486
569
|
return true;
|
|
487
570
|
}
|
|
488
571
|
}
|
|
489
|
-
return false;
|
|
490
|
-
} else if (state.doc.textBetween($from, $to, ' ') === '/') {
|
|
491
|
-
return true;
|
|
492
572
|
}
|
|
573
|
+
|
|
574
|
+
this.isShowingInsert = false;
|
|
493
575
|
return false;
|
|
494
576
|
},
|
|
495
577
|
activateInsertMenuItem(name, info) {
|
|
@@ -523,6 +605,38 @@ export default {
|
|
|
523
605
|
closeInsertMenuItem() {
|
|
524
606
|
this.removeSlash();
|
|
525
607
|
this.activeInsertMenuComponent = null;
|
|
608
|
+
},
|
|
609
|
+
cancelInsertMenuItem() {
|
|
610
|
+
this.doSuppressInsertMenu();
|
|
611
|
+
},
|
|
612
|
+
closeInsertMenu(e) {
|
|
613
|
+
if (
|
|
614
|
+
[ 'ArrowUp', 'ArrowDown', 'Enter', ' ' ].includes(e.key) ||
|
|
615
|
+
this.activeInsertMenuComponent
|
|
616
|
+
) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
this.editor.commands.focus();
|
|
620
|
+
this.activeInsertMenuComponent = null;
|
|
621
|
+
// Only insert character keys
|
|
622
|
+
if (e.key.length === 1) {
|
|
623
|
+
this.editor.commands.insertContent(e.key);
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
focusInsertMenuItem(prev = false, index) {
|
|
627
|
+
if (this.activeInsertMenuComponent) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const buttons = Array.from(this.$refs.insertMenu.$el.querySelectorAll('[data-insert-menu-item]'));
|
|
631
|
+
const currentIndex = buttons.findIndex(el => el === document.activeElement);
|
|
632
|
+
let targetIndex = prev ? currentIndex - 1 : currentIndex + 1;
|
|
633
|
+
if (targetIndex >= buttons.length) {
|
|
634
|
+
targetIndex = 0;
|
|
635
|
+
}
|
|
636
|
+
if (targetIndex < 0) {
|
|
637
|
+
targetIndex = buttons.length - 1;
|
|
638
|
+
}
|
|
639
|
+
buttons[index || targetIndex]?.focus();
|
|
526
640
|
}
|
|
527
641
|
}
|
|
528
642
|
};
|
|
@@ -569,27 +683,41 @@ function traverseNextNode(node) {
|
|
|
569
683
|
background-color: var(--a-base-9);
|
|
570
684
|
}
|
|
571
685
|
|
|
686
|
+
.apos-rich-text-editor__editor ::v-deep .ProseMirror {
|
|
687
|
+
@include apos-transition();
|
|
688
|
+
}
|
|
689
|
+
|
|
572
690
|
.apos-rich-text-editor__editor ::v-deep .ProseMirror:focus {
|
|
573
691
|
outline: none;
|
|
574
692
|
}
|
|
575
693
|
|
|
576
|
-
.apos-rich-text-editor__editor ::v-deep .ProseMirror
|
|
577
|
-
|
|
694
|
+
.apos-rich-text-editor__editor ::v-deep .ProseMirror {
|
|
695
|
+
padding: 10px 0;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
.apos-rich-text-editor__editor ::v-deep .ProseMirror:focus p.apos-is-empty::after {
|
|
699
|
+
display: block;
|
|
700
|
+
margin: 5px 0 10px;
|
|
701
|
+
color: var(--a-primary-transparent-50);
|
|
702
|
+
font-size: var(--a-type-smaller);
|
|
703
|
+
text-transform: uppercase;
|
|
704
|
+
letter-spacing: 0.5px;
|
|
705
|
+
font-weight: 600;
|
|
706
|
+
border-top: 1px solid var(--a-primary-transparent-50);
|
|
707
|
+
padding-top: 5px;
|
|
578
708
|
content: attr(data-placeholder);
|
|
579
|
-
float: left;
|
|
580
709
|
pointer-events: none;
|
|
581
|
-
height: 0;
|
|
582
|
-
color: var(--a-base-4);
|
|
583
710
|
}
|
|
584
711
|
|
|
585
712
|
.apos-rich-text-editor__editor {
|
|
586
713
|
@include apos-transition();
|
|
587
714
|
position: relative;
|
|
588
715
|
border-radius: var(--a-border-radius);
|
|
589
|
-
|
|
716
|
+
background-color: transparent;
|
|
590
717
|
}
|
|
591
718
|
.apos-rich-text-editor__editor.apos-is-visually-empty {
|
|
592
|
-
|
|
719
|
+
background-color: var(--a-primary-transparent-10);
|
|
720
|
+
min-height: 50px;
|
|
593
721
|
}
|
|
594
722
|
.apos-rich-text-editor__editor_after {
|
|
595
723
|
@include type-small;
|
|
@@ -602,9 +730,7 @@ function traverseNextNode(node) {
|
|
|
602
730
|
width: 200px;
|
|
603
731
|
height: 10px;
|
|
604
732
|
margin: auto;
|
|
605
|
-
|
|
606
|
-
margin-bottom: 7.5px;
|
|
607
|
-
color: var(--a-base-5);
|
|
733
|
+
color: var(--a-primary-transparent-50);
|
|
608
734
|
opacity: 0;
|
|
609
735
|
visibility: hidden;
|
|
610
736
|
pointer-events: none;
|
|
@@ -655,12 +781,9 @@ function traverseNextNode(node) {
|
|
|
655
781
|
}
|
|
656
782
|
|
|
657
783
|
.apos-rich-text-insert-menu {
|
|
658
|
-
display: flex;
|
|
659
|
-
flex-direction: column;
|
|
660
784
|
cursor: pointer;
|
|
661
785
|
user-select: none;
|
|
662
|
-
|
|
663
|
-
padding: 16px;
|
|
786
|
+
min-width: 350px;
|
|
664
787
|
border-radius: var(--a-border-radius);
|
|
665
788
|
box-shadow: var(--a-box-shadow);
|
|
666
789
|
background-color: var(--a-background-primary);
|
|
@@ -670,44 +793,75 @@ function traverseNextNode(node) {
|
|
|
670
793
|
font-size: var(--a-type-base);
|
|
671
794
|
}
|
|
672
795
|
|
|
796
|
+
.apos-rich-text-insert-menu-wrapper {
|
|
797
|
+
display: flex;
|
|
798
|
+
flex-direction: column;
|
|
799
|
+
}
|
|
800
|
+
|
|
673
801
|
.apos-rich-text-insert-menu-item {
|
|
802
|
+
all: unset;
|
|
674
803
|
display: flex;
|
|
675
804
|
flex-direction: row;
|
|
676
|
-
|
|
805
|
+
align-items: center;
|
|
806
|
+
gap: 12px;
|
|
807
|
+
padding: 14px 16px;
|
|
808
|
+
border-bottom: 1px solid var(--a-base-9);
|
|
809
|
+
@include apos-transition();
|
|
810
|
+
&:last-of-type {
|
|
811
|
+
border-bottom: none;
|
|
812
|
+
}
|
|
677
813
|
&:hover {
|
|
678
|
-
color: var(--a-
|
|
814
|
+
background-color: var(--a-primary-transparent-10);
|
|
815
|
+
}
|
|
816
|
+
&:active, &:focus {
|
|
817
|
+
background-color: var(--a-primary);
|
|
818
|
+
color: var(--a-white);
|
|
679
819
|
}
|
|
680
820
|
}
|
|
681
821
|
|
|
682
822
|
.apos-rich-text-insert-menu-label {
|
|
683
823
|
display: flex;
|
|
684
824
|
flex-direction: column;
|
|
825
|
+
gap: 5px;
|
|
685
826
|
h4, p {
|
|
686
|
-
margin:
|
|
827
|
+
margin: 0;
|
|
687
828
|
font-family: var(--a-family-default);
|
|
688
|
-
font-size: var(--a-type-base);
|
|
689
829
|
}
|
|
690
830
|
h4 {
|
|
691
|
-
font-weight:
|
|
831
|
+
font-weight: 500;
|
|
832
|
+
font-size: var(--a-type-large);
|
|
833
|
+
}
|
|
834
|
+
p {
|
|
835
|
+
font-size: var(--a-type-label);
|
|
692
836
|
}
|
|
693
837
|
}
|
|
694
838
|
.apos-rich-text-insert-menu-icon {
|
|
695
|
-
// Positions the popover meaningfully
|
|
696
839
|
position: relative;
|
|
840
|
+
display: flex;
|
|
841
|
+
width: 40px;
|
|
842
|
+
height: 40px;
|
|
843
|
+
align-items: center;
|
|
844
|
+
justify-content: center;
|
|
845
|
+
border: 1px solid var(--a-base-8);
|
|
846
|
+
color: var(--a-text-primary);
|
|
847
|
+
background-color: var(--a-white);
|
|
848
|
+
border-radius: var(--a-border-radius);
|
|
697
849
|
}
|
|
698
850
|
|
|
699
851
|
.apos-rich-text-insert-menu-heading {
|
|
700
|
-
|
|
852
|
+
padding: 12px 16px;
|
|
853
|
+
background-color: var(--a-base-9);
|
|
854
|
+
color: var(--a-base-2);
|
|
855
|
+
font-weight: 500;
|
|
856
|
+
border-bottom: 1px solid var(--a-base-7);
|
|
857
|
+
font-size: var(--a-type-label);
|
|
858
|
+
letter-spacing: 0.25px;
|
|
701
859
|
}
|
|
702
860
|
|
|
703
861
|
::v-deep .ProseMirror {
|
|
704
862
|
> * + * {
|
|
705
863
|
margin-top: 0.75em;
|
|
706
864
|
}
|
|
707
|
-
|
|
708
|
-
> :last-child {
|
|
709
|
-
margin-bottom: 1.75em;
|
|
710
|
-
}
|
|
711
865
|
}
|
|
712
866
|
|
|
713
867
|
::v-deep .ProseMirror-gapcursor {
|
|
@@ -95,9 +95,8 @@ module.exports = (self) => {
|
|
|
95
95
|
|
|
96
96
|
self.addFieldType({
|
|
97
97
|
name: 'string',
|
|
98
|
-
convert
|
|
98
|
+
convert(req, field, data, destination) {
|
|
99
99
|
destination[field.name] = self.apos.launder.string(data[field.name], field.def);
|
|
100
|
-
|
|
101
100
|
destination[field.name] = checkStringLength(destination[field.name], field.min, field.max);
|
|
102
101
|
// If field is required but empty (and client side didn't catch that)
|
|
103
102
|
// This is new and until now if JS client side failed, then it would
|
|
@@ -105,8 +104,16 @@ module.exports = (self) => {
|
|
|
105
104
|
if (field.required && (_.isUndefined(data[field.name]) || !data[field.name].toString().length)) {
|
|
106
105
|
throw self.apos.error('required');
|
|
107
106
|
}
|
|
107
|
+
|
|
108
|
+
if (field.pattern) {
|
|
109
|
+
const regex = new RegExp(field.pattern);
|
|
110
|
+
|
|
111
|
+
if (!regex.test(destination[field.name])) {
|
|
112
|
+
throw self.apos.error('invalid');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
108
115
|
},
|
|
109
|
-
index
|
|
116
|
+
index(value, field, texts) {
|
|
110
117
|
const silent = field.silent === undefined ? true : field.silent;
|
|
111
118
|
texts.push({
|
|
112
119
|
weight: field.weight || 15,
|
|
@@ -114,10 +121,22 @@ module.exports = (self) => {
|
|
|
114
121
|
silent: silent
|
|
115
122
|
});
|
|
116
123
|
},
|
|
117
|
-
isEmpty
|
|
124
|
+
isEmpty(field, value) {
|
|
118
125
|
return !value.length;
|
|
119
126
|
},
|
|
120
|
-
|
|
127
|
+
validate(field, options, warn, fail) {
|
|
128
|
+
if (!field.pattern) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const isRegexInstance = field.pattern instanceof RegExp;
|
|
133
|
+
if (!isRegexInstance && typeof field.pattern !== 'string') {
|
|
134
|
+
fail('The pattern property must be a RegExp or a String');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
field.pattern = isRegexInstance ? field.pattern.source : field.pattern;
|
|
138
|
+
},
|
|
139
|
+
addQueryBuilder(field, query) {
|
|
121
140
|
query.addBuilder(field.name, {
|
|
122
141
|
finalize: function () {
|
|
123
142
|
if (self.queryBuilderInterested(query, field.name)) {
|
|
@@ -150,6 +169,9 @@ module.exports = (self) => {
|
|
|
150
169
|
if (field.page) {
|
|
151
170
|
options.allow = '/';
|
|
152
171
|
}
|
|
172
|
+
if (data.aposIsTemplate) {
|
|
173
|
+
options.allow = field.page ? [ '/', '@' ] : '@';
|
|
174
|
+
}
|
|
153
175
|
destination[field.name] = self.apos.util.slugify(self.apos.launder.string(data[field.name], field.def), options);
|
|
154
176
|
|
|
155
177
|
if (field.page) {
|
|
@@ -756,9 +778,9 @@ module.exports = (self) => {
|
|
|
756
778
|
const { name: uniqueFieldName, label: uniqueFieldLabel } = field.schema.find(subfield => subfield.unique) || [];
|
|
757
779
|
if (uniqueFieldName) {
|
|
758
780
|
const duplicates = data
|
|
759
|
-
.map(item => Array.isArray(item[uniqueFieldName])
|
|
781
|
+
.map(item => (Array.isArray(item[uniqueFieldName])
|
|
760
782
|
? item[uniqueFieldName][0]._id
|
|
761
|
-
: item[uniqueFieldName])
|
|
783
|
+
: item[uniqueFieldName]))
|
|
762
784
|
.filter((item, index, array) => array.indexOf(item) !== index);
|
|
763
785
|
if (duplicates.length) {
|
|
764
786
|
throw self.apos.error('duplicate', `${req.t(uniqueFieldLabel)} in ${req.t(field.label)} must be unique`);
|
|
@@ -978,6 +1000,9 @@ module.exports = (self) => {
|
|
|
978
1000
|
},
|
|
979
1001
|
|
|
980
1002
|
relate: async function (req, field, objects, options) {
|
|
1003
|
+
if ((!self.apos.doc?.replicateReached) && (!field.idsStorage)) {
|
|
1004
|
+
self.apos.util.warnDevOnce('premature-relationship-query', 'Database queries for types with relationships may fail if made before the @apostrophecms/doc:beforeReplicate event');
|
|
1005
|
+
}
|
|
981
1006
|
return self.relationshipDriver(req, joinr.byArray, false, objects, field.idsStorage, field.fieldsStorage, field.name, options);
|
|
982
1007
|
},
|
|
983
1008
|
|
|
@@ -1109,6 +1134,9 @@ module.exports = (self) => {
|
|
|
1109
1134
|
name: 'relationshipReverse',
|
|
1110
1135
|
vueComponent: false,
|
|
1111
1136
|
relate: async function (req, field, objects, options) {
|
|
1137
|
+
if ((!self.apos.doc?.replicateReached) && (!field.idsStorage)) {
|
|
1138
|
+
self.apos.util.warnDevOnce('premature-relationship-query', 'Database queries for types with relationships may fail if made before the @apostrophecms/doc:beforeReplicate event');
|
|
1139
|
+
}
|
|
1112
1140
|
return self.relationshipDriver(req, joinr.byArrayReverse, true, objects, field.idsStorage, field.fieldsStorage, field.name, options);
|
|
1113
1141
|
},
|
|
1114
1142
|
validate: function (field, options, warn, fail) {
|
|
@@ -190,9 +190,13 @@ export default {
|
|
|
190
190
|
const options = {
|
|
191
191
|
def: ''
|
|
192
192
|
};
|
|
193
|
-
|
|
193
|
+
|
|
194
|
+
if (this.field.aposIsTemplate) {
|
|
195
|
+
options.allow = this.field.page ? [ '/', '@' ] : '@';
|
|
196
|
+
} else if (this.field.page && !componentOnly) {
|
|
194
197
|
options.allow = '/';
|
|
195
198
|
}
|
|
199
|
+
|
|
196
200
|
let preserveDash = false;
|
|
197
201
|
// When you are typing a slug it feels wrong for hyphens you typed
|
|
198
202
|
// to disappear as you go, so if the last character is not valid in a slug,
|
|
@@ -200,10 +204,12 @@ export default {
|
|
|
200
204
|
if (this.focus && s.length && (sluggo(s.charAt(s.length - 1), options) === '')) {
|
|
201
205
|
preserveDash = true;
|
|
202
206
|
}
|
|
207
|
+
|
|
203
208
|
s = sluggo(s, options);
|
|
204
209
|
if (preserveDash) {
|
|
205
210
|
s += '-';
|
|
206
211
|
}
|
|
212
|
+
|
|
207
213
|
if (this.field.page && !componentOnly) {
|
|
208
214
|
if (!this.followingValues?.title) {
|
|
209
215
|
const nextParts = this.next.split('/');
|
|
@@ -226,6 +232,7 @@ export default {
|
|
|
226
232
|
s += '/';
|
|
227
233
|
}
|
|
228
234
|
}
|
|
235
|
+
|
|
229
236
|
if (!componentOnly) {
|
|
230
237
|
s = this.setPrefix(s);
|
|
231
238
|
}
|
|
@@ -262,7 +269,20 @@ export default {
|
|
|
262
269
|
// doc editor modal it will momentarily be tracked as archived but
|
|
263
270
|
// without not have the archive prefix, so check that too.
|
|
264
271
|
updated = this.isArchived && archivePrefix ? `${archivePrefix}${updated}` : updated;
|
|
272
|
+
} else if (this.field.aposIsTemplate) {
|
|
273
|
+
let prefix = '';
|
|
274
|
+
if (this.field.page) {
|
|
275
|
+
if (!updated.startsWith('/@')) {
|
|
276
|
+
prefix = '/@';
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
if (!updated.startsWith('@')) {
|
|
280
|
+
prefix = '@';
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
updated = prefix + updated;
|
|
265
284
|
}
|
|
285
|
+
|
|
266
286
|
return updated;
|
|
267
287
|
},
|
|
268
288
|
async checkConflict() {
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
v-if="field.textarea && field.type === 'string'" rows="5"
|
|
12
12
|
v-model="next" :placeholder="$t(field.placeholder)"
|
|
13
13
|
@keydown.enter="enterEmit"
|
|
14
|
-
:disabled="field.readOnly"
|
|
14
|
+
:disabled="field.readOnly"
|
|
15
|
+
:required="field.required"
|
|
15
16
|
:id="uid" :tabindex="tabindex"
|
|
16
17
|
/>
|
|
17
18
|
<input
|
|
@@ -123,12 +124,9 @@ export default {
|
|
|
123
124
|
if (typeof value === 'string' && !value.length) {
|
|
124
125
|
// Also correct for float and integer because Vue coerces
|
|
125
126
|
// number fields to either a number or the empty string
|
|
126
|
-
|
|
127
|
-
return 'required';
|
|
128
|
-
} else {
|
|
129
|
-
return false;
|
|
130
|
-
}
|
|
127
|
+
return this.field.required ? 'required' : false;
|
|
131
128
|
}
|
|
129
|
+
|
|
132
130
|
const minMaxFields = [
|
|
133
131
|
'integer',
|
|
134
132
|
'float',
|
|
@@ -137,6 +135,13 @@ export default {
|
|
|
137
135
|
'password'
|
|
138
136
|
];
|
|
139
137
|
|
|
138
|
+
if (typeof value === 'string' && this.field.pattern) {
|
|
139
|
+
const regex = new RegExp(this.field.pattern);
|
|
140
|
+
if (!regex.test(value)) {
|
|
141
|
+
return 'invalid';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
140
145
|
if (this.field.min && minMaxFields.includes(this.field.type)) {
|
|
141
146
|
if ((value != null) && value.length && (this.minMaxComparable(value) < this.field.min)) {
|
|
142
147
|
return 'min';
|
|
@@ -192,7 +197,7 @@ export default {
|
|
|
192
197
|
},
|
|
193
198
|
minMaxComparable(s) {
|
|
194
199
|
const converted = this.convert(s);
|
|
195
|
-
if (
|
|
200
|
+
if ([ 'integer', 'float', 'date', 'range', 'time' ].includes(this.field.type)) {
|
|
196
201
|
// Compare the actual values for these types
|
|
197
202
|
return converted;
|
|
198
203
|
} else {
|
|
@@ -158,6 +158,7 @@ export default {
|
|
|
158
158
|
this.schema.forEach(item => {
|
|
159
159
|
fields[item.name] = {};
|
|
160
160
|
fields[item.name].field = item;
|
|
161
|
+
fields[item.name].field.aposIsTemplate = this.value?.data?.aposIsTemplate;
|
|
161
162
|
fields[item.name].value = {
|
|
162
163
|
data: this.value[item.name]
|
|
163
164
|
};
|