apostrophe 3.52.0 → 3.53.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 +60 -2
- package/defaults.js +1 -0
- package/index.js +3 -2
- package/lib/check-if-conditions.js +44 -0
- package/lib/moog-require.js +23 -1
- package/modules/@apostrophecms/admin-bar/index.js +30 -1
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +4 -1
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +14 -8
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaExpandedMenu.vue +8 -2
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +4 -0
- package/modules/@apostrophecms/command-menu/ui/apos/components/AposCommandMenuShortcut.vue +1 -0
- package/modules/@apostrophecms/doc/index.js +13 -7
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +36 -22
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +35 -27
- package/modules/@apostrophecms/i18n/i18n/en.json +8 -0
- package/modules/@apostrophecms/i18n/index.js +49 -2
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaUploader.vue +16 -1
- package/modules/@apostrophecms/login/index.js +5 -1
- package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +2 -0
- package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +37 -40
- package/modules/@apostrophecms/modal/ui/apos/components/AposModalConfirm.vue +1 -2
- package/modules/@apostrophecms/modal/ui/apos/components/AposModalShareDraft.vue +3 -2
- package/modules/@apostrophecms/modal/ui/apos/components/AposModalTabs.vue +4 -5
- package/modules/@apostrophecms/modal/ui/apos/mixins/AposFocusMixin.js +91 -0
- package/modules/@apostrophecms/modal/ui/apos/mixins/AposModalTabsMixin.js +16 -4
- package/modules/@apostrophecms/page/ui/apos/components/AposPagesManager.vue +9 -3
- package/modules/@apostrophecms/piece-type/index.js +1 -1
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +2 -0
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +1 -1
- package/modules/@apostrophecms/schema/index.js +13 -0
- package/modules/@apostrophecms/schema/lib/addFieldTypes.js +3 -10
- package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +0 -1
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +1 -15
- package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +20 -13
- package/modules/@apostrophecms/schema/ui/apos/components/AposSubform.vue +164 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposSubform.js +141 -0
- package/modules/@apostrophecms/settings/index.js +627 -0
- package/modules/@apostrophecms/settings/ui/apos/apps/TheAposSettings.js +8 -0
- package/modules/@apostrophecms/settings/ui/apos/components/AposSettingsManager.vue +162 -0
- package/modules/@apostrophecms/settings/ui/apos/logic/AposSettingsManager.js +169 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +10 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposButtonSplit.vue +23 -6
- package/modules/@apostrophecms/ui/ui/apos/components/AposCellLabels.vue +1 -7
- package/modules/@apostrophecms/ui/ui/apos/components/AposSubformPreview.vue +136 -0
- package/modules/@apostrophecms/ui/ui/apos/lib/i18next.js +6 -6
- package/modules/@apostrophecms/ui/ui/apos/mixins/AposCellMixin.js +9 -0
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_admin.scss +9 -0
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_widgets.scss +5 -1
- package/modules/@apostrophecms/user/index.js +30 -3
- package/package.json +1 -1
- package/test/i18n.js +168 -0
- package/test/settings.js +544 -0
|
@@ -303,8 +303,13 @@
|
|
|
303
303
|
"pages": "Pages",
|
|
304
304
|
"parentNotLocalized": "Localize the parent page first",
|
|
305
305
|
"password": "Password",
|
|
306
|
+
"passwordChangeHelp": "Modify your existing password",
|
|
307
|
+
"passwordCurrent": "Current Password",
|
|
308
|
+
"passwordCurrentError": "Current password is incorrect",
|
|
306
309
|
"passwordErrorMax": "Maximum of {{ max }} characters",
|
|
307
310
|
"passwordErrorMin": "Minimum of {{ min }} characters",
|
|
311
|
+
"passwordNew": "New Password",
|
|
312
|
+
"passwordRepeat": "Repeat New Password",
|
|
308
313
|
"passwordResetRequest": "Your request to reset your password on {{ site }}",
|
|
309
314
|
"pasteWidget": "Paste {{ widget }}",
|
|
310
315
|
"pending": "Pending",
|
|
@@ -420,6 +425,7 @@
|
|
|
420
425
|
"selectPage": "Select Page",
|
|
421
426
|
"selectedMenuItem": "✓ {{ label }}",
|
|
422
427
|
"sentenceJoiner": " ",
|
|
428
|
+
"settings": "Personal Settings",
|
|
423
429
|
"shareDraft": "Share Draft",
|
|
424
430
|
"shareDraftCopyLink": "Copy draft link",
|
|
425
431
|
"shareDraftDescription": "Enabling draft sharing will allow anyone with this link to view the current draft",
|
|
@@ -464,6 +470,8 @@
|
|
|
464
470
|
"type": "Type",
|
|
465
471
|
"typeWithCount": "{{ type }} ({{ count }})",
|
|
466
472
|
"unableToSwitchModes": "Unable to switch modes.",
|
|
473
|
+
"uiLanguageLabel": "UI Language",
|
|
474
|
+
"uiLanguageWebsite": "Same as Website",
|
|
467
475
|
"undo": "Undo",
|
|
468
476
|
"undoFailed": "The operation could not be undone.",
|
|
469
477
|
"undoPublish": "Undo Publish",
|
|
@@ -4,6 +4,34 @@
|
|
|
4
4
|
//
|
|
5
5
|
// `apos.i18n.i18next` can be used to directly access the `i18next` npm module instance if necessary.
|
|
6
6
|
// It usually is not necessary. Use `req.t` if you need to localize in a route.
|
|
7
|
+
//
|
|
8
|
+
// ## Options
|
|
9
|
+
//
|
|
10
|
+
// ### `locales` TODO
|
|
11
|
+
//
|
|
12
|
+
// ### `defaultLocale` TODO
|
|
13
|
+
//
|
|
14
|
+
// ### `adminLocales`
|
|
15
|
+
//
|
|
16
|
+
// Controls what admin UI language can be set per user. If set, `adminLocale` user field
|
|
17
|
+
// will be automatically added to the user schema.
|
|
18
|
+
// Contains an array of objects with `label` and `value` properties:
|
|
19
|
+
// ```js
|
|
20
|
+
// {
|
|
21
|
+
// label: 'English',
|
|
22
|
+
// value: 'en'
|
|
23
|
+
// }
|
|
24
|
+
// ```
|
|
25
|
+
//
|
|
26
|
+
// ### `defaultAdminLocale`
|
|
27
|
+
//
|
|
28
|
+
// The default admin UI language. If `adminLocales` are configured, it should
|
|
29
|
+
// should match a `value` property from the list. Furthermore, it will be used
|
|
30
|
+
// as the default value for the`adminLocale` user field. If it is not set,
|
|
31
|
+
// but `adminLocales` is set, then the default is to display the admin UI
|
|
32
|
+
// in the same language as the website content.
|
|
33
|
+
// Example: `defaultLocale: 'fr'`.
|
|
34
|
+
//
|
|
7
35
|
|
|
8
36
|
const i18next = require('i18next');
|
|
9
37
|
const fs = require('fs');
|
|
@@ -55,6 +83,12 @@ module.exports = {
|
|
|
55
83
|
self.locales = self.getLocales();
|
|
56
84
|
self.hostnamesInUse = Object.values(self.locales).find(locale => locale.hostname);
|
|
57
85
|
self.defaultLocale = self.options.defaultLocale || Object.keys(self.locales)[0];
|
|
86
|
+
// Contains label/value object for each locale
|
|
87
|
+
self.adminLocales = self.options.adminLocales || [];
|
|
88
|
+
// Contains only the string value of the default admin locale (e.g. 'en').
|
|
89
|
+
// If adminLocales are configured, it should be one of them. Otherwise,
|
|
90
|
+
// it can be any valid locale string identifier.
|
|
91
|
+
self.defaultAdminLocale = self.options.defaultAdminLocale || null;
|
|
58
92
|
// Lint the locale configurations
|
|
59
93
|
for (const [ key, options ] of Object.entries(self.locales)) {
|
|
60
94
|
if (!options) {
|
|
@@ -70,6 +104,15 @@ module.exports = {
|
|
|
70
104
|
throw self.apos.error('invalid', `Locale prefixes must not contain more than one forward slash ("/").\nUse hyphens as separators. Check locale "${key}".`);
|
|
71
105
|
}
|
|
72
106
|
}
|
|
107
|
+
if (!Array.isArray(self.adminLocales)) {
|
|
108
|
+
throw self.apos.error('invalid', 'The "adminLocales" option must be an array.');
|
|
109
|
+
}
|
|
110
|
+
if (self.defaultAdminLocale && typeof self.defaultAdminLocale !== 'string') {
|
|
111
|
+
throw self.apos.error('invalid', 'The "defaultAdminLocale" option must be a string.');
|
|
112
|
+
}
|
|
113
|
+
if (self.defaultAdminLocale && self.adminLocales.length && !self.adminLocales.some(al => al.value === self.defaultAdminLocale)) {
|
|
114
|
+
throw self.apos.error('invalid', `The value of "defaultAdminLocale" "${self.defaultAdminLocale}" doesn't match any of the existing "adminLocales" values.`);
|
|
115
|
+
}
|
|
73
116
|
const fallbackLng = [ self.defaultLocale ];
|
|
74
117
|
// In case the default locale also has inadequate admin UI phrases
|
|
75
118
|
if (fallbackLng[0] !== 'en') {
|
|
@@ -574,10 +617,13 @@ module.exports = {
|
|
|
574
617
|
}
|
|
575
618
|
},
|
|
576
619
|
getBrowserData(req) {
|
|
620
|
+
const adminLocale = req.user?.adminLocale === ''
|
|
621
|
+
? req.locale
|
|
622
|
+
: req.user?.adminLocale || self.defaultAdminLocale || req.locale;
|
|
577
623
|
const i18n = {
|
|
578
|
-
[
|
|
624
|
+
[adminLocale]: self.getBrowserBundles(adminLocale)
|
|
579
625
|
};
|
|
580
|
-
if (
|
|
626
|
+
if (adminLocale !== self.defaultLocale) {
|
|
581
627
|
i18n[self.defaultLocale] = self.getBrowserBundles(self.defaultLocale);
|
|
582
628
|
}
|
|
583
629
|
// In case the default locale also has inadequate admin UI phrases
|
|
@@ -587,6 +633,7 @@ module.exports = {
|
|
|
587
633
|
const result = {
|
|
588
634
|
i18n,
|
|
589
635
|
locale: req.locale,
|
|
636
|
+
adminLocale,
|
|
590
637
|
defaultLocale: self.defaultLocale,
|
|
591
638
|
defaultNamespace: self.defaultNamespace,
|
|
592
639
|
locales: self.locales,
|
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
@dragenter="incrementDragover"
|
|
8
8
|
@dragleave="decrementDragover"
|
|
9
9
|
>
|
|
10
|
-
<div
|
|
10
|
+
<div
|
|
11
|
+
class="apos-media-uploader__inner"
|
|
12
|
+
tabindex="0"
|
|
13
|
+
@keydown="onUploadDragAndDropKeyDown"
|
|
14
|
+
>
|
|
11
15
|
<AposCloudUploadIcon
|
|
12
16
|
class="apos-media-uploader__icon"
|
|
13
17
|
/>
|
|
@@ -30,6 +34,7 @@
|
|
|
30
34
|
:accept="accept"
|
|
31
35
|
multiple="true"
|
|
32
36
|
:disabled="disabled"
|
|
37
|
+
tabindex="-1"
|
|
33
38
|
>
|
|
34
39
|
</label>
|
|
35
40
|
</template>
|
|
@@ -222,6 +227,16 @@ export default {
|
|
|
222
227
|
});
|
|
223
228
|
}
|
|
224
229
|
}
|
|
230
|
+
},
|
|
231
|
+
// Trigger the file input click (via `this.create`) when pressing Enter or Space
|
|
232
|
+
// of the drag&drop area, which is made focusable unlike the input file.
|
|
233
|
+
onUploadDragAndDropKeyDown(e) {
|
|
234
|
+
const isEnterPressed = e.key === 'Enter' || e.code === 'Enter' || e.code === 'NumpadEnter';
|
|
235
|
+
const isSpaceBarPressed = e.keyCode === 32 || e.code === 'Space';
|
|
236
|
+
|
|
237
|
+
if (isEnterPressed || isSpaceBarPressed) {
|
|
238
|
+
this.create();
|
|
239
|
+
}
|
|
225
240
|
}
|
|
226
241
|
}
|
|
227
242
|
};
|
|
@@ -892,7 +892,11 @@ module.exports = {
|
|
|
892
892
|
if (err) {
|
|
893
893
|
return callback(err);
|
|
894
894
|
}
|
|
895
|
-
|
|
895
|
+
try {
|
|
896
|
+
await self.emit('afterSessionLogin', req);
|
|
897
|
+
} catch (e) {
|
|
898
|
+
return callback(e);
|
|
899
|
+
}
|
|
896
900
|
// Make sure no handler removed req.user
|
|
897
901
|
if (req.user) {
|
|
898
902
|
// Mark the login timestamp. Middleware takes care of ensuring
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
:icon="checkboxIcon"
|
|
12
12
|
@click="selectAll"
|
|
13
13
|
ref="selectAll"
|
|
14
|
+
data-apos-test="selectAll"
|
|
14
15
|
/>
|
|
15
16
|
<div
|
|
16
17
|
v-for="{
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
<AposButton
|
|
26
27
|
v-if="!operations"
|
|
27
28
|
:label="label"
|
|
29
|
+
:action="action"
|
|
28
30
|
:icon="icon"
|
|
29
31
|
:disabled="!checkedCount"
|
|
30
32
|
:modifiers="['small']"
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
aria-modal="true"
|
|
13
13
|
:aria-labelledby="id"
|
|
14
14
|
ref="modalEl"
|
|
15
|
+
@keydown="cycleElementsToFocus"
|
|
16
|
+
@focus.capture="storeFocusedElement"
|
|
15
17
|
data-apos-modal
|
|
16
18
|
>
|
|
17
19
|
<transition :name="transitionType">
|
|
@@ -92,8 +94,13 @@
|
|
|
92
94
|
// So as the modal exits, they should change in reverse. `showModal` becomes
|
|
93
95
|
// `false`, then `active` is set to `false` once the modal has finished its
|
|
94
96
|
// transition.
|
|
97
|
+
import AposFocusMixin from 'Modules/@apostrophecms/modal/mixins/AposFocusMixin';
|
|
98
|
+
|
|
95
99
|
export default {
|
|
96
100
|
name: 'AposModal',
|
|
101
|
+
mixins: [
|
|
102
|
+
AposFocusMixin
|
|
103
|
+
],
|
|
97
104
|
props: {
|
|
98
105
|
modal: {
|
|
99
106
|
type: Object,
|
|
@@ -123,8 +130,11 @@ export default {
|
|
|
123
130
|
return 'fade';
|
|
124
131
|
}
|
|
125
132
|
},
|
|
126
|
-
|
|
127
|
-
return this.modal.
|
|
133
|
+
shouldTrapFocus() {
|
|
134
|
+
return this.modal.trapFocus || this.modal.trapFocus === undefined;
|
|
135
|
+
},
|
|
136
|
+
triggerFocusRefresh () {
|
|
137
|
+
return this.modal.triggerFocusRefresh;
|
|
128
138
|
},
|
|
129
139
|
hasBeenLocalized: function() {
|
|
130
140
|
return Object.keys(apos.i18n.locales).length > 1;
|
|
@@ -187,12 +197,19 @@ export default {
|
|
|
187
197
|
}
|
|
188
198
|
},
|
|
189
199
|
watch: {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
200
|
+
// Simple way to re-trigger focusable elements
|
|
201
|
+
// that might have been created or removed
|
|
202
|
+
// after an update, like an XHR call to get the
|
|
203
|
+
// pieces list in the AposDocsManager modal, for instance.
|
|
204
|
+
triggerFocusRefresh (newVal) {
|
|
205
|
+
if (this.shouldTrapFocus) {
|
|
206
|
+
this.$nextTick(this.trapFocus);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
mounted() {
|
|
211
|
+
if (this.shouldTrapFocus) {
|
|
212
|
+
this.$nextTick(this.trapFocus);
|
|
196
213
|
}
|
|
197
214
|
},
|
|
198
215
|
methods: {
|
|
@@ -217,6 +234,7 @@ export default {
|
|
|
217
234
|
// pop doesn't quite suffice because of race conditions when
|
|
218
235
|
// closing one and opening another
|
|
219
236
|
apos.modal.stack = apos.modal.stack.filter(modal => modal !== this);
|
|
237
|
+
this.focusLastModalFocusedElement();
|
|
220
238
|
},
|
|
221
239
|
bindEventListeners () {
|
|
222
240
|
window.addEventListener('keydown', this.onKeydown);
|
|
@@ -232,47 +250,26 @@ export default {
|
|
|
232
250
|
this.$emit('esc');
|
|
233
251
|
},
|
|
234
252
|
trapFocus () {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const focusableElements = [
|
|
238
|
-
'button',
|
|
253
|
+
const elementSelectors = [
|
|
254
|
+
'[tabindex]',
|
|
239
255
|
'[href]',
|
|
240
256
|
'input',
|
|
241
257
|
'select',
|
|
242
258
|
'textarea',
|
|
243
|
-
'
|
|
259
|
+
'button'
|
|
244
260
|
];
|
|
245
|
-
const focusableString = focusableElements.join(', ');
|
|
246
|
-
const modalEl = this.$refs.modalEl;
|
|
247
|
-
const focusables = modalEl.querySelectorAll(focusableString);
|
|
248
|
-
const firstFocusableElement = focusables[0];
|
|
249
|
-
const lastFocusableElement = focusables[focusables.length - 1];
|
|
250
261
|
|
|
251
|
-
|
|
262
|
+
const selector = elementSelectors
|
|
263
|
+
.map(addExcludingAttributes)
|
|
264
|
+
.join(', ');
|
|
252
265
|
|
|
253
|
-
|
|
266
|
+
this.elementsToFocus = [ ...this.$refs.modalEl.querySelectorAll(selector) ]
|
|
267
|
+
.filter(this.isElementVisible);
|
|
254
268
|
|
|
255
|
-
|
|
256
|
-
const isTabPressed = e.key === 'Tab' || e.code === 'Tab';
|
|
257
|
-
if (!isTabPressed) {
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
269
|
+
this.focusElement(this.focusedElement, this.elementsToFocus[0]);
|
|
260
270
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (document.activeElement === firstFocusableElement) {
|
|
264
|
-
// Add focus for the last focusable element
|
|
265
|
-
lastFocusableElement.focus();
|
|
266
|
-
e.preventDefault();
|
|
267
|
-
}
|
|
268
|
-
} else {
|
|
269
|
-
// If tab key is pressed
|
|
270
|
-
if (document.activeElement === lastFocusableElement) {
|
|
271
|
-
// Add focus for the first focusable element
|
|
272
|
-
firstFocusableElement.focus();
|
|
273
|
-
e.preventDefault();
|
|
274
|
-
}
|
|
275
|
-
}
|
|
271
|
+
function addExcludingAttributes(element) {
|
|
272
|
+
return `${element}:not([tabindex="-1"]):not([disabled]):not([type="hidden"]):not([aria-hidden])`;
|
|
276
273
|
}
|
|
277
274
|
}
|
|
278
275
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
</h2>
|
|
18
18
|
<Close
|
|
19
19
|
class="apos-share-draft__close"
|
|
20
|
+
tabindex="0"
|
|
20
21
|
:title="$t('apostrophe:close')"
|
|
21
22
|
:size="18"
|
|
22
23
|
@click.prevent="close"
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
v-model="shareUrl"
|
|
49
50
|
type="text"
|
|
50
51
|
disabled
|
|
52
|
+
tabindex="-1"
|
|
51
53
|
class="apos-share-draft__url"
|
|
52
54
|
>
|
|
53
55
|
<a
|
|
@@ -93,8 +95,7 @@ export default {
|
|
|
93
95
|
active: false,
|
|
94
96
|
type: 'overlay',
|
|
95
97
|
showModal: false,
|
|
96
|
-
disableHeader: true
|
|
97
|
-
trapFocus: true
|
|
98
|
+
disableHeader: true
|
|
98
99
|
},
|
|
99
100
|
shareUrl: '',
|
|
100
101
|
disabled: true
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
<div class="apos-modal-tabs">
|
|
3
3
|
<ul class="apos-modal-tabs__tabs">
|
|
4
4
|
<li
|
|
5
|
-
class="apos-modal-tabs__tab"
|
|
5
|
+
class="apos-modal-tabs__tab"
|
|
6
|
+
v-for="tab in tabs"
|
|
6
7
|
:key="tab.name"
|
|
8
|
+
v-show="tab.isVisible !== false"
|
|
7
9
|
>
|
|
8
10
|
<button
|
|
9
11
|
:id="tab.name" class="apos-modal-tabs__btn"
|
|
10
|
-
:aria-selected="tab.name ===
|
|
12
|
+
:aria-selected="tab.name === current ? true : false"
|
|
11
13
|
@click="selectTab"
|
|
12
14
|
>
|
|
13
15
|
{{ $t(tab.label) }}
|
|
@@ -41,9 +43,6 @@ export default {
|
|
|
41
43
|
},
|
|
42
44
|
emits: [ 'select-tab' ],
|
|
43
45
|
computed: {
|
|
44
|
-
currentTab() {
|
|
45
|
-
return this.current || this.tabs[0].name;
|
|
46
|
-
},
|
|
47
46
|
tabErrors() {
|
|
48
47
|
const errors = {};
|
|
49
48
|
for (const key in this.errors) {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Provides:
|
|
3
|
+
*
|
|
4
|
+
* Methods to handle focus with keyboard.
|
|
5
|
+
*/
|
|
6
|
+
export default {
|
|
7
|
+
data() {
|
|
8
|
+
return {
|
|
9
|
+
elementsToFocus: [],
|
|
10
|
+
|
|
11
|
+
// specific to modals:
|
|
12
|
+
focusedElement: null
|
|
13
|
+
};
|
|
14
|
+
},
|
|
15
|
+
methods: {
|
|
16
|
+
// Adapted from https://uxdesign.cc/how-to-trap-focus-inside-modal-to-make-it-ada-compliant-6a50f9a70700
|
|
17
|
+
// All the elements inside modal which you want to make focusable.
|
|
18
|
+
//
|
|
19
|
+
// This has been adapted to Vue logic with `this.elementsToFocus` array as a data
|
|
20
|
+
// so that any elements, not only from a modal but a menu for instance, can be focusable.
|
|
21
|
+
// `cycleElementsToFocus` listeners relies on this dynamic list which has the advantage of
|
|
22
|
+
// taking new or less elements to focus, after an update has happened inside a modal,
|
|
23
|
+
// like an XHR call to get the pieces list in the AposDocsManager modal, for instance.
|
|
24
|
+
cycleElementsToFocus(e) {
|
|
25
|
+
if (!this.elementsToFocus.length) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const isTabPressed = e.key === 'Tab' || e.code === 'Tab';
|
|
30
|
+
if (!isTabPressed) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const firstElementToFocus = this.elementsToFocus.at(0);
|
|
35
|
+
const lastElementToFocus = this.elementsToFocus.at(-1);
|
|
36
|
+
|
|
37
|
+
// If shift key pressed for shift + tab combination
|
|
38
|
+
if (e.shiftKey) {
|
|
39
|
+
if (document.activeElement === firstElementToFocus) {
|
|
40
|
+
// Add focus for the last focusable element
|
|
41
|
+
lastElementToFocus.focus();
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// If tab key is pressed
|
|
48
|
+
if (document.activeElement === lastElementToFocus) {
|
|
49
|
+
// Add focus for the first focusable element
|
|
50
|
+
firstElementToFocus.focus();
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
// Focus the last focused element from the last modal.
|
|
55
|
+
// If it is not focusable (not visible/not in the DOM),
|
|
56
|
+
// fallbacks to the first focusable element from the last modal.
|
|
57
|
+
focusLastModalFocusedElement() {
|
|
58
|
+
const lastModal = apos.modal.stack.at(-1);
|
|
59
|
+
|
|
60
|
+
if (!lastModal) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const { focusedElement, elementsToFocus } = lastModal;
|
|
65
|
+
|
|
66
|
+
this.focusElement(focusedElement, elementsToFocus[0]);
|
|
67
|
+
},
|
|
68
|
+
storeFocusedElement(e) {
|
|
69
|
+
this.focusedElement = e.target;
|
|
70
|
+
},
|
|
71
|
+
// Iterate through elements given in arguments and
|
|
72
|
+
// focus the first element that exists in the DOM.
|
|
73
|
+
focusElement(...elementsToFocus) {
|
|
74
|
+
for (const element of elementsToFocus) {
|
|
75
|
+
const isAlreadySelected = document.activeElement === element;
|
|
76
|
+
|
|
77
|
+
if (!element || !this.isElementVisible(element)) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (!isAlreadySelected) {
|
|
81
|
+
element.focus();
|
|
82
|
+
}
|
|
83
|
+
// Element exists in the DOM and is focused, stop iterating.
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
isElementVisible(element) {
|
|
88
|
+
return element.offsetParent !== null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
@@ -46,28 +46,40 @@ export default {
|
|
|
46
46
|
const tabs = [];
|
|
47
47
|
for (const key in this.groups) {
|
|
48
48
|
if (key !== 'utility') {
|
|
49
|
+
// AposRelationshipEditor does not implement AposEditorMixin with the function conditionalFields
|
|
50
|
+
const conditionalFields = this.conditionalFields?.('other') || [];
|
|
51
|
+
const fields = this.groups[key].fields;
|
|
49
52
|
tabs.push({
|
|
50
53
|
name: key,
|
|
51
54
|
label: this.groups[key].label,
|
|
52
|
-
fields
|
|
55
|
+
fields,
|
|
56
|
+
isVisible: fields.some(field => conditionalFields[field] !== false)
|
|
53
57
|
});
|
|
54
58
|
}
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
return tabs;
|
|
62
|
+
},
|
|
63
|
+
firstVisibleTabName() {
|
|
64
|
+
const { name = null } = this.tabs.find(tab => tab.isVisible === true) || this.tabs[0] || {};
|
|
65
|
+
|
|
66
|
+
return name;
|
|
58
67
|
}
|
|
59
68
|
},
|
|
60
69
|
|
|
61
70
|
watch: {
|
|
62
71
|
tabs() {
|
|
63
|
-
if (
|
|
64
|
-
this.currentTab
|
|
72
|
+
if (
|
|
73
|
+
!this.currentTab ||
|
|
74
|
+
!this.tabs.some(tab => tab.isVisible === true && tab.name === this.currentTab)
|
|
75
|
+
) {
|
|
76
|
+
this.currentTab = this.firstVisibleTabName;
|
|
65
77
|
}
|
|
66
78
|
}
|
|
67
79
|
},
|
|
68
80
|
|
|
69
81
|
mounted() {
|
|
70
|
-
this.currentTab = this.
|
|
82
|
+
this.currentTab = this.firstVisibleTabName;
|
|
71
83
|
},
|
|
72
84
|
methods: {
|
|
73
85
|
switchPane(id) {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<AposModal
|
|
3
|
-
:modal="modal"
|
|
4
|
-
|
|
5
|
-
@
|
|
3
|
+
:modal="modal"
|
|
4
|
+
modal-title="apostrophe:managePages"
|
|
5
|
+
@esc="confirmAndCancel"
|
|
6
|
+
@no-modal="$emit('safe-close')"
|
|
7
|
+
@inactive="modal.active = false"
|
|
8
|
+
@show-modal="modal.showModal = true"
|
|
6
9
|
>
|
|
7
10
|
<template #secondaryControls>
|
|
8
11
|
<AposButton
|
|
@@ -99,6 +102,7 @@ export default {
|
|
|
99
102
|
moduleName: '@apostrophecms/page',
|
|
100
103
|
modal: {
|
|
101
104
|
active: false,
|
|
105
|
+
triggerFocusRefresh: 0,
|
|
102
106
|
type: 'slide',
|
|
103
107
|
showModal: false,
|
|
104
108
|
width: 'two-thirds'
|
|
@@ -221,6 +225,8 @@ export default {
|
|
|
221
225
|
// Get the data. This will be more complex in actuality.
|
|
222
226
|
this.modal.active = true;
|
|
223
227
|
await this.getPages();
|
|
228
|
+
this.modal.triggerFocusRefresh++;
|
|
229
|
+
|
|
224
230
|
apos.bus.$on('content-changed', this.getPages);
|
|
225
231
|
apos.bus.$on('command-menu-manager-create-new', this.create);
|
|
226
232
|
apos.bus.$on('command-menu-manager-close', this.confirmAndCancel);
|
|
@@ -179,7 +179,7 @@ module.exports = {
|
|
|
179
179
|
label: 'apostrophe:restore',
|
|
180
180
|
messages: {
|
|
181
181
|
progress: 'Restoring {{ type }}...',
|
|
182
|
-
completed: '
|
|
182
|
+
completed: 'Restored {{ count }} {{ type }}.'
|
|
183
183
|
},
|
|
184
184
|
icon: 'archive-arrow-up-icon',
|
|
185
185
|
if: {
|
|
@@ -137,6 +137,7 @@ export default {
|
|
|
137
137
|
return {
|
|
138
138
|
modal: {
|
|
139
139
|
active: false,
|
|
140
|
+
triggerFocusRefresh: 0,
|
|
140
141
|
type: 'overlay',
|
|
141
142
|
showModal: false
|
|
142
143
|
},
|
|
@@ -230,6 +231,7 @@ export default {
|
|
|
230
231
|
this.modal.active = true;
|
|
231
232
|
await this.getPieces();
|
|
232
233
|
await this.getAllPiecesTotal();
|
|
234
|
+
this.modal.triggerFocusRefresh++;
|
|
233
235
|
|
|
234
236
|
apos.bus.$on('content-changed', this.getPieces);
|
|
235
237
|
apos.bus.$on('command-menu-manager-create-new', this.create);
|
package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue
CHANGED
|
@@ -596,7 +596,7 @@ export default {
|
|
|
596
596
|
const { $to } = state.selection;
|
|
597
597
|
if (state.selection.empty && $to?.nodeBefore?.text) {
|
|
598
598
|
const text = $to.nodeBefore.text;
|
|
599
|
-
if (text === '/') {
|
|
599
|
+
if (text.slice(-1) === '/') {
|
|
600
600
|
const pos = this.editor.view.state.selection.$anchor.pos;
|
|
601
601
|
// Select the slash so an insert operation can replace it
|
|
602
602
|
this.editor.commands.setTextSelection({
|
|
@@ -1240,6 +1240,9 @@ module.exports = {
|
|
|
1240
1240
|
if (!field.label && !field.contextual) {
|
|
1241
1241
|
field.label = _.startCase(field.name.replace(/^_/, ''));
|
|
1242
1242
|
}
|
|
1243
|
+
if (field.hidden && field.hidden !== true && field.hidden !== false) {
|
|
1244
|
+
fail(`hidden must be a boolean, "${field.hidden}" provided.`);
|
|
1245
|
+
}
|
|
1243
1246
|
if (field.if && field.if.$or && !Array.isArray(field.if.$or)) {
|
|
1244
1247
|
fail(`$or conditional must be an array of conditions. Current $or configuration: ${JSON.stringify(field.if.$or)}`);
|
|
1245
1248
|
}
|
|
@@ -1631,6 +1634,16 @@ module.exports = {
|
|
|
1631
1634
|
} catch (error) {
|
|
1632
1635
|
throw self.apos.error('invalid', error.message);
|
|
1633
1636
|
}
|
|
1637
|
+
},
|
|
1638
|
+
|
|
1639
|
+
getSlugFieldOptions(field, data) {
|
|
1640
|
+
const options = {
|
|
1641
|
+
def: field.def
|
|
1642
|
+
};
|
|
1643
|
+
if (field.page) {
|
|
1644
|
+
options.allow = '/';
|
|
1645
|
+
}
|
|
1646
|
+
return options;
|
|
1634
1647
|
}
|
|
1635
1648
|
};
|
|
1636
1649
|
},
|
|
@@ -162,16 +162,9 @@ module.exports = (self) => {
|
|
|
162
162
|
// if field.page is true, expect a page slug (slashes allowed,
|
|
163
163
|
// leading slash required). Otherwise, expect a object-style slug
|
|
164
164
|
// (no slashes at all)
|
|
165
|
-
convert
|
|
166
|
-
const options =
|
|
167
|
-
|
|
168
|
-
};
|
|
169
|
-
if (field.page) {
|
|
170
|
-
options.allow = '/';
|
|
171
|
-
}
|
|
172
|
-
if (data.aposIsTemplate) {
|
|
173
|
-
options.allow = field.page ? [ '/', '@' ] : '@';
|
|
174
|
-
}
|
|
165
|
+
convert (req, field, data, destination) {
|
|
166
|
+
const options = self.getSlugFieldOptions(field, data);
|
|
167
|
+
|
|
175
168
|
destination[field.name] = self.apos.util.slugify(self.apos.launder.string(data[field.name], field.def), options);
|
|
176
169
|
|
|
177
170
|
if (field.page) {
|
|
@@ -191,9 +191,7 @@ export default {
|
|
|
191
191
|
def: ''
|
|
192
192
|
};
|
|
193
193
|
|
|
194
|
-
if (this.field.
|
|
195
|
-
options.allow = this.field.page ? [ '/', '@' ] : '@';
|
|
196
|
-
} else if (this.field.page && !componentOnly) {
|
|
194
|
+
if (this.field.page && !componentOnly) {
|
|
197
195
|
options.allow = '/';
|
|
198
196
|
}
|
|
199
197
|
|
|
@@ -269,18 +267,6 @@ export default {
|
|
|
269
267
|
// doc editor modal it will momentarily be tracked as archived but
|
|
270
268
|
// without not have the archive prefix, so check that too.
|
|
271
269
|
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;
|
|
284
270
|
}
|
|
285
271
|
|
|
286
272
|
return updated;
|