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
@@ -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
- focused(newVal) {
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
- // For this contextual widget, no need to check `widget.aposPlaceholder` value
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
- // Avoid brief display of the placeholder when loading the page.
213
- if (this.isFocused === null) {
214
- return '';
215
- }
216
-
217
- // Display placeholder after loading the page.
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:first-child::before {
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
- generation: 1,
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.getAttributes('img').src || this.active;
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
- if (this.active) {
142
- this.active = false;
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: false,
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;
@@ -430,6 +430,7 @@ function alwaysExpand(field) {
430
430
  position: relative;
431
431
  left: -35px;
432
432
  min-width: calc(100% + 35px);
433
+ width: max-content;
433
434
  margin: 0 0 $spacing-base;
434
435
  border-collapse: collapse;
435
436
 
@@ -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 (this.subfields[doc._id] && !Object.keys(doc._fields || {}).length) {
190
- doc._fields = this.subfields[doc._id];
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)) {