apostrophe 4.26.0 → 4.27.0-alpha.1

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 (34) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/lib/locales.js +10 -0
  3. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBar.vue +11 -1
  4. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +12 -1
  5. package/modules/@apostrophecms/box-field/index.js +4 -0
  6. package/modules/@apostrophecms/box-field/ui/apos/components/AposInputBox.vue +2 -2
  7. package/modules/@apostrophecms/box-field/ui/apos/logic/AposInputBox.js +12 -4
  8. package/modules/@apostrophecms/command-menu/ui/apos/components/TheAposCommandMenu.vue +11 -3
  9. package/modules/@apostrophecms/i18n/index.js +25 -3
  10. package/modules/@apostrophecms/modal/ui/apos/apps/AposModals.js +1 -0
  11. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +6 -1
  12. package/modules/@apostrophecms/notification/ui/apos/components/AposNotification.vue +8 -1
  13. package/modules/@apostrophecms/rich-text-widget/index.js +12 -0
  14. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +21 -2
  15. package/modules/@apostrophecms/schema/lib/addFieldTypes.js +36 -0
  16. package/modules/@apostrophecms/schema/ui/apos/components/AposInputDateAndTime.vue +2 -2
  17. package/modules/@apostrophecms/schema/ui/apos/components/AposInputPassword.vue +1 -0
  18. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRange.vue +1 -1
  19. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputDateAndTime.js +12 -3
  20. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputPassword.js +7 -4
  21. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRange.js +6 -2
  22. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputSlug.js +34 -14
  23. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputString.js +16 -8
  24. package/modules/@apostrophecms/schema/ui/apos/mixins/AposFieldDirection.js +16 -0
  25. package/modules/@apostrophecms/template/views/outerLayoutBase.html +1 -1
  26. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +17 -1
  27. package/modules/@apostrophecms/ui/ui/apos/scss/global/_utilities.scss +8 -0
  28. package/modules/@apostrophecms/ui/ui/apos/stores/modal.js +57 -0
  29. package/package.json +5 -5
  30. package/test/content-i18n.js +7 -5
  31. package/test/i18n.js +77 -5
  32. package/test/modules/default-page/views/page.html +1 -1
  33. package/test/modules/i18n-test-page/views/page.html +6 -0
  34. package/test/schemas.js +392 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.27.0-alpha.1
4
+
5
+ ### Adds
6
+
7
+ - In `@apostrophecms/i18n` add `direction` property to locale configuration to support RTL languages (e.g., `he`, `ar`), `slugDirection` option to control default direction of slug fields. Add `direction` property in the schema field definitions to override and validate text direction (supports `ltr` and `rtl`) of input fields. Note that the admin UI layout and labels overall are still LTR only for now, but these changes accommodate editing RTL locale content within that. For best results the feature should be combined with `adminLocales` and `defaultAdminLocale` module options, e.g. the admin UI itself should remain in English or another LTR language for now.
8
+
9
+ ## 4.26.1-alpha.1
10
+
11
+ ### Fixes
12
+
13
+ - Fix a bug where rich text images and permalinks are not properly rendered in public external front (e.g. Astro) views.
14
+
3
15
  ## 4.26.0
4
16
 
5
17
  ### Adds
package/lib/locales.js CHANGED
@@ -10,6 +10,16 @@ module.exports = {
10
10
  const hostname = options.hostname || '__none';
11
11
  const prefix = options.prefix || '__none';
12
12
  const key = `${hostname}:${prefix}`;
13
+ const direction = options.direction;
14
+
15
+ // Note that default `ltr` directions should have been set
16
+ // already by the `getLocales` method in the i18n module.
17
+ if ([ 'ltr', 'rtl' ].indexOf(direction) === -1) {
18
+ throw new Error(stripIndent`
19
+ The locale "${name}" has an invalid direction option "${direction || 'undefined'}".
20
+ The direction option must be either "ltr" (left to right) or "rtl" (right to left).
21
+ `);
22
+ }
13
23
 
14
24
  hostnamesCount += options.hostname ? 1 : 0;
15
25
 
@@ -10,7 +10,7 @@
10
10
  />
11
11
  <nav
12
12
  ref="adminBar"
13
- class="apos-admin-bar"
13
+ :class="classes"
14
14
  role="menubar"
15
15
  aria-label="Apostrophe Admin Bar"
16
16
  >
@@ -36,7 +36,9 @@
36
36
  </template>
37
37
 
38
38
  <script>
39
+ import { mapState } from 'pinia';
39
40
  import AposThemeMixin from 'Modules/@apostrophecms/ui/mixins/AposThemeMixin';
41
+ import { useModalStore } from 'Modules/@apostrophecms/ui/stores/modal';
40
42
 
41
43
  export default {
42
44
  name: 'TheAposAdminBar',
@@ -48,6 +50,7 @@ export default {
48
50
  }
49
51
  },
50
52
  computed: {
53
+ ...mapState(useModalStore, [ 'getAdminDirectionClass' ]),
51
54
  menuItems() {
52
55
  return this.items.filter(item => !item.options?.user);
53
56
  },
@@ -59,6 +62,13 @@ export default {
59
62
  },
60
63
  bars() {
61
64
  return this.moduleOptions.bars;
65
+ },
66
+ classes() {
67
+ const directionClass = this.getAdminDirectionClass();
68
+ return {
69
+ 'apos-admin-bar': true,
70
+ [directionClass]: !!directionClass
71
+ };
62
72
  }
63
73
  },
64
74
  mounted() {
@@ -149,6 +149,7 @@
149
149
  :is="widgetEditorComponent(widget.type)"
150
150
  v-if="isContextual && !foreign"
151
151
  :key="generation"
152
+ :class="adminContentDirectionClass"
152
153
  :options="widgetOptions"
153
154
  :type="widget.type"
154
155
  :model-value="widget"
@@ -163,6 +164,7 @@
163
164
  v-else
164
165
  :id="widget._id"
165
166
  :key="`${generation}-preview`"
167
+ :class="adminContentDirectionClass"
166
168
  :options="widgetOptions"
167
169
  :type="widget.type"
168
170
  :area-field-id="fieldId"
@@ -209,6 +211,8 @@
209
211
  import { mapState, mapActions } from 'pinia';
210
212
  import { useWidgetStore } from 'Modules/@apostrophecms/ui/stores/widget';
211
213
  import { useBreakpointPreviewStore } from 'Modules/@apostrophecms/ui/stores/breakpointPreview.js';
214
+ import { useModalStore } from 'Modules/@apostrophecms/ui/stores/modal.js';
215
+
212
216
  export default {
213
217
  name: 'AposAreaWidget',
214
218
  props: {
@@ -356,6 +360,9 @@ export default {
356
360
  'emphasizedWidgets'
357
361
  ]),
358
362
  ...mapState(useBreakpointPreviewStore, { breakpointPreviewMode: 'mode' }),
363
+ adminContentDirectionClass() {
364
+ return this.getAdminContentDirectionClass();
365
+ },
359
366
  // Passed only to the preview layer (custom preview components).
360
367
  followingValuesWithParent() {
361
368
  return Object.entries(this.followingValues || {})
@@ -440,7 +447,10 @@ export default {
440
447
  },
441
448
  labelsClasses() {
442
449
  return {
443
- [this.classes.show]: this.isHovered || this.isFocused || this.isEmphasized
450
+ [this.classes.show]: this.isHovered || this.isFocused || this.isEmphasized,
451
+ // Force LTR for breadcrumbs for now so that nested AreaWidgets
452
+ // behave properly.
453
+ 'apos-ltr': true
444
454
  };
445
455
  },
446
456
  addClasses() {
@@ -544,6 +554,7 @@ export default {
544
554
  },
545
555
  methods: {
546
556
  ...mapActions(useWidgetStore, [ 'setFocusedWidget', 'setHoveredWidget' ]),
557
+ ...mapActions(useModalStore, [ 'getAdminContentDirectionClass' ]),
547
558
  // Emits same actions as the native operations,
548
559
  // e.g ('edit', { index }), ('remove', { index }), etc.
549
560
  onOperation({ name, payload }) {
@@ -64,6 +64,10 @@ module.exports = {
64
64
  const defProps = [ 'top', 'right', 'bottom', 'left' ];
65
65
  let defMin = 0;
66
66
 
67
+ if (field.direction && ![ 'ltr', 'rtl' ].includes(field.direction)) {
68
+ fail('The direction property must be "ltr" or "rtl" if specified');
69
+ }
70
+
67
71
  if (field.max && typeof field.max !== 'number') {
68
72
  fail('Property "max" must be a number');
69
73
  }
@@ -16,7 +16,7 @@
16
16
  v-model="shorthand"
17
17
  type="number"
18
18
  placeholder="--"
19
- class="apos-input-box__shorthand-input apos-input apos-input--number"
19
+ :class="classesShorthand"
20
20
  data-apos-test="box-input-all"
21
21
  :aria-label="`${$t(field.label)} ${$t('apostrophe:boxFieldAriaLabelAll')}`"
22
22
  :disabled="field.readOnly || field.disabled || mode === 'individual'"
@@ -96,7 +96,7 @@
96
96
  class="apos-input-box__individual-input apos-input apos-input--number"
97
97
  :data-apos-test="`box-input-side-${side}`"
98
98
  :aria-label="`${$t(field.label)} ${$t('apostrophe:boxFieldAriaLabelIndividual', { side })}`"
99
- :class="`apos-input-box__individual-input--${side}`"
99
+ :class="[ `apos-input-box__individual-input--${side}`, ...classesIndividual ]"
100
100
  :disabled="field.readOnly || field.disabled"
101
101
  :min="field.min"
102
102
  :max="field.max ? field.max : null"
@@ -1,8 +1,9 @@
1
1
  import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin';
2
+ import AposFieldDirectionMixin from 'Modules/@apostrophecms/schema/mixins/AposFieldDirection.js';
2
3
 
3
4
  export default {
4
5
  name: 'AposInputBox',
5
- mixins: [ AposInputMixin ],
6
+ mixins: [ AposInputMixin, AposFieldDirectionMixin ],
6
7
  emits: [ 'return' ],
7
8
  props: {
8
9
  },
@@ -38,11 +39,18 @@ export default {
38
39
  tabindex () {
39
40
  return this.field.disableFocus ? '-1' : '0';
40
41
  },
41
- classes() {
42
+ classesShorthand() {
42
43
  return [
44
+ 'apos-input-box__shorthand-input',
43
45
  'apos-input',
44
- 'apos-input--box'
45
- ];
46
+ 'apos-input--number',
47
+ this.directionClass
48
+ ].filter(Boolean);
49
+ },
50
+ classesIndividual() {
51
+ return [
52
+ this.directionClass
53
+ ].filter(Boolean);
46
54
  }
47
55
  },
48
56
  mounted() {
@@ -1,7 +1,6 @@
1
1
  <template>
2
2
  <div
3
- class="apos-command-menu"
4
- :class="themeClass"
3
+ :class="classes"
5
4
  >
6
5
  <!-- nothing for the moment -->
7
6
  </div>
@@ -30,7 +29,16 @@ export default {
30
29
  };
31
30
  },
32
31
  computed: {
33
- ...mapState(useModalStore, [ 'stack' ]),
32
+ ...mapState(useModalStore, [ 'stack', 'getAdminDirectionClass' ]),
33
+ classes() {
34
+ const directionClass = this.getAdminDirectionClass();
35
+ const classes = this.themeClass;
36
+ classes.push('apos-command-menu');
37
+ if (directionClass) {
38
+ classes.push(directionClass);
39
+ }
40
+ return classes;
41
+ },
34
42
  shortcuts() {
35
43
  const modals = Object.values(this.modals[this.modal] || {});
36
44
 
@@ -32,6 +32,11 @@
32
32
  // ### `encoding`
33
33
  //
34
34
  // Defaults to `'utf-8'`. You almost certainly do not want to change this.
35
+ //
36
+ // ### `slugDirection`
37
+ //
38
+ // Controls the default `direction` value of slug schema. Can be `ltr`, `rtl` or
39
+ // `undefined|null` to not set a default. Defaults to `ltr`.
35
40
 
36
41
  const i18next = require('i18next');
37
42
  const fs = require('fs');
@@ -77,7 +82,8 @@ module.exports = {
77
82
  // If true, slugifying will strip accents from Latin characters
78
83
  stripUrlAccents: false,
79
84
  // You almost certainly do not want to change this
80
- encoding: 'utf-8'
85
+ encoding: 'utf-8',
86
+ slugDirection: 'ltr'
81
87
  },
82
88
  async init(self) {
83
89
  self.defaultNamespace = 'default';
@@ -93,6 +99,12 @@ module.exports = {
93
99
  // If adminLocales are configured, it should be one of them. Otherwise,
94
100
  // it can be any valid locale string identifier.
95
101
  self.defaultAdminLocale = self.options.defaultAdminLocale || null;
102
+ if (self.options.slugDirection && ![ 'ltr', 'rtl' ].includes(self.options.slugDirection)) {
103
+ throw self.apos.error(
104
+ 'invalid',
105
+ `The "slugDirection" option of "${self.__meta.name}" module must be "ltr", "rtl" or "null".`
106
+ );
107
+ }
96
108
  // Lint the locale configurations
97
109
  for (const [ key, options ] of Object.entries(self.locales)) {
98
110
  if (!options) {
@@ -175,6 +187,12 @@ module.exports = {
175
187
  // The array is provided in the order in which locales are configured.
176
188
  // The current locale is included and has the property `current: true`.
177
189
  async addLocalizations(req) {
190
+ const locale = req.locale || self.defaultLocale;
191
+ req.data.i18n = {
192
+ locale,
193
+ direction: self.locales[locale]?.direction || 'ltr',
194
+ label: self.locales[locale]?.label || locale
195
+ };
178
196
  const context = req.data.piece || req.data.page;
179
197
  if (!context) {
180
198
  return;
@@ -212,6 +230,7 @@ module.exports = {
212
230
  const info = doc || {};
213
231
  info.locale = name;
214
232
  info.label = self.locales[name].label;
233
+ info.direction = self.locales[name].direction;
215
234
  info.homePageUrl = `${localeReq.prefix}/`;
216
235
  req.data.localizations.push(info);
217
236
  }
@@ -686,7 +705,8 @@ module.exports = {
686
705
  show: self.show,
687
706
  action: self.action,
688
707
  crossDomainClipboard: req.session && req.session.aposCrossDomainClipboard,
689
- stripUrlAccents: self.options.stripUrlAccents
708
+ stripUrlAccents: self.options.stripUrlAccents,
709
+ slugDirection: self.options.slugDirection
690
710
  };
691
711
  if (req.session && req.session.aposCrossDomainClipboard) {
692
712
  req.session.aposCrossDomainClipboard = null;
@@ -715,11 +735,13 @@ module.exports = {
715
735
  getLocales() {
716
736
  const locales = self.options.locales || {
717
737
  en: {
718
- label: 'English'
738
+ label: 'English',
739
+ direction: 'ltr'
719
740
  }
720
741
  };
721
742
  for (const locale in locales) {
722
743
  locales[locale]._edit = true;
744
+ locales[locale].direction ??= 'ltr';
723
745
  }
724
746
  verifyLocales(locales, self.apos.options.baseUrl);
725
747
  return locales;
@@ -18,6 +18,7 @@ export default function() {
18
18
  apos.modal.getProperties = modalStore.getProperties;
19
19
  apos.modal.onTopOf = modalStore.onTopOf;
20
20
  apos.modal.getActiveLocale = modalStore.getActiveLocale;
21
+ apos.modal.getAdminDirectionClass = modalStore.getAdminDirectionClass;
21
22
  apos.confirm = modalStore.confirm;
22
23
  apos.alert = modalStore.alert;
23
24
  apos.report = modalStore.report;
@@ -211,6 +211,7 @@ function hasSlot(slotName) {
211
211
  }
212
212
 
213
213
  const classes = computed(() => {
214
+ const directionClass = store.getAdminDirectionClass();
214
215
  const classes = [ 'apos-modal' ];
215
216
  classes.push(`apos-modal--${props.modal.type}`);
216
217
  if (props.modal.type === 'slide') {
@@ -224,7 +225,11 @@ const classes = computed(() => {
224
225
  if (props.modal.busy) {
225
226
  classes.push('apos-modal--busy');
226
227
  }
227
- return classes.join(' ');
228
+ if (directionClass) {
229
+ classes.push(directionClass);
230
+ }
231
+
232
+ return classes;
228
233
  });
229
234
 
230
235
  const innerClasses = computed(() => {
@@ -66,6 +66,7 @@ import {
66
66
  } from 'vue';
67
67
  import Close from '@apostrophecms/vue-material-design-icons/Close.vue';
68
68
  import { useNotificationStore } from 'Modules/@apostrophecms/ui/stores/notification.js';
69
+ import { useModalStore } from 'Modules/@apostrophecms/ui/stores/modal.js';
69
70
 
70
71
  const props = defineProps({
71
72
  notification: {
@@ -77,6 +78,7 @@ const props = defineProps({
77
78
  const $t = inject('i18n');
78
79
  const emit = defineEmits([ 'close' ]);
79
80
  const store = useNotificationStore();
81
+ const modalStore = useModalStore();
80
82
 
81
83
  const hasJob = props.notification.job && props.notification.job._id;
82
84
  const job = ref(
@@ -96,6 +98,7 @@ const process = computed(() => {
96
98
 
97
99
  const classList = computed(() => {
98
100
  const classes = [ 'apos-notification' ];
101
+ const directionClass = modalStore.getAdminDirectionClass();
99
102
 
100
103
  if (Array.isArray(props.notification.classes) && props.notification.classes.length) {
101
104
  classes.push(...props.notification.classes);
@@ -119,7 +122,11 @@ const classList = computed(() => {
119
122
  classes.push('apos-notification--long');
120
123
  }
121
124
 
122
- return classes.join(' ');
125
+ if (directionClass) {
126
+ classes.push(directionClass);
127
+ }
128
+
129
+ return classes;
123
130
  });
124
131
 
125
132
  const iconComponent = computed(() => {
@@ -1152,6 +1152,18 @@ module.exports = {
1152
1152
  };
1153
1153
  return _super(req, _widget, options, _with);
1154
1154
  },
1155
+ annotateWidgetForExternalFront(_super, widget, { scene } = {}) {
1156
+ if (scene !== 'apos') {
1157
+ let content = widget.content || '';
1158
+ content = self.linkPermalinks(widget, content);
1159
+ content = self.linkImages(widget, content);
1160
+ // It should be safe to modify the widget data here because
1161
+ // the external front-end data is read-only. The Admin UI
1162
+ // and actual data is not affected.
1163
+ widget.content = content;
1164
+ }
1165
+ return _super(widget, { scene });
1166
+ },
1155
1167
  // Add on the core default options to use, if needed.
1156
1168
  getBrowserData(_super, req) {
1157
1169
  const initialData = _super(req);
@@ -24,7 +24,7 @@
24
24
  >
25
25
  <AposContextMenuDialog
26
26
  menu-placement="top"
27
- class-list="apos-rich-text-toolbar"
27
+ :class-list="contextMenuClasses"
28
28
  :has-tip="false"
29
29
  >
30
30
  <div
@@ -51,7 +51,7 @@
51
51
  :id="`insert-menu-${modelValue._id}`"
52
52
  ref="insertMenu"
53
53
  plugin-key="insertMenu"
54
- class="apos-rich-text-insert-menu"
54
+ :class="insertMenuClasses"
55
55
  :tippy-options="{ duration: 100, zIndex: 999, placement: 'bottom-start' }"
56
56
  :should-show="showFloatingMenu"
57
57
  :editor="editor"
@@ -115,6 +115,7 @@
115
115
  </template>
116
116
 
117
117
  <script>
118
+ import { mapState } from 'pinia';
118
119
  import {
119
120
  Editor,
120
121
  EditorContent,
@@ -153,6 +154,7 @@ import { klona } from 'klona';
153
154
  import newInstance from 'apostrophe/modules/@apostrophecms/schema/lib/newInstance.js';
154
155
  import merge from 'lodash/merge';
155
156
  import { useAposStyles } from 'Modules/@apostrophecms/styles/composables/AposStyles.js';
157
+ import { useModalStore } from 'Modules/@apostrophecms/ui/stores/modal';
156
158
 
157
159
  export default {
158
160
  name: 'AposRichTextWidgetEditor',
@@ -222,6 +224,23 @@ export default {
222
224
  };
223
225
  },
224
226
  computed: {
227
+ ...mapState(useModalStore, [ 'getAdminDirectionClass' ]),
228
+ // Note that context menu class-list expects a string
229
+ contextMenuClasses() {
230
+ const directionClass = this.getAdminDirectionClass();
231
+ const classes = [ 'apos-rich-text-toolbar' ];
232
+ if (directionClass) {
233
+ classes.push(directionClass);
234
+ }
235
+ return classes.join(' ');
236
+ },
237
+ insertMenuClasses() {
238
+ const directionClass = this.getAdminDirectionClass();
239
+ return {
240
+ 'apos-rich-text-insert-menu': true,
241
+ [directionClass]: !!directionClass
242
+ };
243
+ },
225
244
  tableOptions() {
226
245
  const options = this.moduleOptions.tableOptions || {};
227
246
 
@@ -147,6 +147,10 @@ module.exports = (self) => {
147
147
  return !value.length;
148
148
  },
149
149
  validate(field, options, warn, fail) {
150
+ if (field.direction && !_.includes([ 'ltr', 'rtl' ], field.direction)) {
151
+ fail('The direction property must be "ltr" or "rtl" if specified');
152
+ }
153
+
150
154
  if (!field.pattern) {
151
155
  return;
152
156
  }
@@ -455,6 +459,11 @@ module.exports = (self) => {
455
459
  destination[field.name] = null;
456
460
  }
457
461
  },
462
+ validate(field, options, warn, fail) {
463
+ if (field.direction && !_.includes([ 'ltr', 'rtl' ], field.direction)) {
464
+ fail('The direction property must be "ltr" or "rtl" if specified');
465
+ }
466
+ },
458
467
  addQueryBuilder(field, query) {
459
468
  return query.addBuilder(field.name, {
460
469
  finalize: function () {
@@ -514,6 +523,11 @@ module.exports = (self) => {
514
523
  destination[field.name] = null;
515
524
  }
516
525
  },
526
+ validate(field, options, warn, fail) {
527
+ if (field.direction && !_.includes([ 'ltr', 'rtl' ], field.direction)) {
528
+ fail('The direction property must be "ltr" or "rtl" if specified');
529
+ }
530
+ },
517
531
  addQueryBuilder(field, query) {
518
532
  return query.addBuilder(field.name, {
519
533
  finalize: function () {
@@ -567,6 +581,11 @@ module.exports = (self) => {
567
581
  }
568
582
  }
569
583
  destination[field.name] = data[field.name];
584
+ },
585
+ validate(field, options, warn, fail) {
586
+ if (field.direction && !_.includes([ 'ltr', 'rtl' ], field.direction)) {
587
+ fail('The direction property must be "ltr" or "rtl" if specified');
588
+ }
570
589
  }
571
590
  });
572
591
 
@@ -600,6 +619,10 @@ module.exports = (self) => {
600
619
  return '';
601
620
  },
602
621
  validate(field, options, warn, fail) {
622
+ if (field.direction && !_.includes([ 'ltr', 'rtl' ], field.direction)) {
623
+ fail('The direction property must be "ltr" or "rtl" if specified');
624
+ }
625
+
603
626
  if (!field.pattern) {
604
627
  return;
605
628
  }
@@ -659,6 +682,9 @@ module.exports = (self) => {
659
682
  destination[field.name] = self.apos.launder.date(newDateVal);
660
683
  },
661
684
  validate: function (field, options, warn, fail) {
685
+ if (field.direction && !_.includes([ 'ltr', 'rtl' ], field.direction)) {
686
+ fail('The direction property must be "ltr" or "rtl" if specified');
687
+ }
662
688
  if (field.max && !field.max.match(dateRegex)) {
663
689
  fail('Property "max" must be in the date format, YYYY-MM-DD');
664
690
  }
@@ -723,6 +749,11 @@ module.exports = (self) => {
723
749
  destination[field.name] = data[field.name]
724
750
  ? self.apos.launder.date(data[field.name])
725
751
  : null;
752
+ },
753
+ validate: function (field, options, warn, fail) {
754
+ if (field.direction && !_.includes([ 'ltr', 'rtl' ], field.direction)) {
755
+ fail('The direction property must be "ltr" or "rtl" if specified');
756
+ }
726
757
  }
727
758
  });
728
759
 
@@ -741,6 +772,11 @@ module.exports = (self) => {
741
772
  );
742
773
  }
743
774
  },
775
+ validate: function (field, options, warn, fail) {
776
+ if (field.direction && !_.includes([ 'ltr', 'rtl' ], field.direction)) {
777
+ fail('The direction property must be "ltr" or "rtl" if specified');
778
+ }
779
+ },
744
780
  def: ''
745
781
  });
746
782
 
@@ -17,7 +17,7 @@
17
17
  <input
18
18
  v-model="date"
19
19
  class="apos-input apos-input--date"
20
- :class="{'apos-input--disabled': disabled}"
20
+ :class="classes"
21
21
  type="date"
22
22
  @change="setDateAndTime"
23
23
  >
@@ -27,7 +27,7 @@
27
27
  <input
28
28
  v-model="time"
29
29
  class="apos-input apos-input--time"
30
- :class="{'apos-input--disabled': disabled}"
30
+ :class="classes"
31
31
  type="time"
32
32
  @change="setDateAndTime"
33
33
  >
@@ -13,6 +13,7 @@
13
13
  v-model="next"
14
14
  type="password"
15
15
  class="apos-input apos-input--password"
16
+ :class="classes"
16
17
  :placeholder="$t(field.placeholder)"
17
18
  :disabled="field.readOnly"
18
19
  :required="field.required"
@@ -41,7 +41,7 @@
41
41
  :min="field.min"
42
42
  :max="field.max"
43
43
  :step="field.step"
44
- class="apos-range__input"
44
+ :class="classes"
45
45
  :disabled="field.readOnly"
46
46
  >
47
47
  <div
@@ -1,8 +1,9 @@
1
- import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin';
2
1
  import dayjs from 'dayjs';
2
+ import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin.js';
3
+ import AposFieldDirectionMixin from 'Modules/@apostrophecms/schema/mixins/AposFieldDirection.js';
3
4
 
4
5
  export default {
5
- mixins: [ AposInputMixin ],
6
+ mixins: [ AposInputMixin, AposFieldDirectionMixin ],
6
7
  emits: [ 'return' ],
7
8
  data() {
8
9
  return {
@@ -12,7 +13,7 @@ export default {
12
13
  disabled: !this.field.required
13
14
  };
14
15
  },
15
- mounted () {
16
+ mounted() {
16
17
  this.initDateAndTime();
17
18
  },
18
19
  watch: {
@@ -25,6 +26,14 @@ export default {
25
26
  }
26
27
  }
27
28
  },
29
+ computed: {
30
+ classes() {
31
+ return [
32
+ { 'apos-input--disabled': this.disabled },
33
+ this.directionClass
34
+ ].filter(Boolean);
35
+ }
36
+ },
28
37
  methods: {
29
38
  toggle() {
30
39
  this.disabled = !this.disabled;
@@ -1,13 +1,16 @@
1
-
2
- import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin';
1
+ import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin.js';
2
+ import AposFieldDirectionMixin from 'Modules/@apostrophecms/schema/mixins/AposFieldDirection.js';
3
3
 
4
4
  export default {
5
5
  name: 'AposInputPassword',
6
- mixins: [ AposInputMixin ],
6
+ mixins: [ AposInputMixin, AposFieldDirectionMixin ],
7
7
  emits: [ 'return' ],
8
8
  computed: {
9
- tabindex () {
9
+ tabindex() {
10
10
  return this.field.disableFocus ? '-1' : '0';
11
+ },
12
+ classes() {
13
+ return [ this.directionClass ].filter(Boolean);
11
14
  }
12
15
  },
13
16
  methods: {
@@ -1,4 +1,4 @@
1
- import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin';
1
+ import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin.js';
2
2
 
3
3
  export default {
4
4
  name: 'AposInputRange',
@@ -34,6 +34,9 @@ export default {
34
34
  } else {
35
35
  return false;
36
36
  }
37
+ },
38
+ classes() {
39
+ return [ 'apos-range__input' ];
37
40
  }
38
41
  },
39
42
  methods: {
@@ -57,7 +60,8 @@ export default {
57
60
  const min = this.$refs.range.min;
58
61
  const max = this.$refs.range.max;
59
62
  const val = this.next < min ? min : this.next;
60
- this.$refs.range.style.backgroundSize = (val - min) * 100 / (max - min) + '% 100%';
63
+ this.$refs.range.style.backgroundSize =
64
+ ((val - min) * 100) / (max - min) + '% 100%';
61
65
  },
62
66
  validate(value) {
63
67
  if (this.field.required) {