apostrophe 4.10.0 → 4.11.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 +20 -0
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +2 -2
- package/modules/@apostrophecms/asset/lib/webpack/apos/webpack.scss.js +0 -9
- package/modules/@apostrophecms/color-field/index.js +1 -1
- package/modules/@apostrophecms/color-field/ui/apos/components/AposColor.vue +9 -1
- package/modules/@apostrophecms/color-field/ui/apos/lib/AposColorEditableInput.vue +11 -3
- package/modules/@apostrophecms/color-field/ui/apos/lib/AposColorSaturation.vue +0 -1
- package/modules/@apostrophecms/color-field/ui/apos/logic/AposInputColor.js +8 -2
- package/modules/@apostrophecms/color-field/ui/apos/mixins/AposColorMixin.js +4 -0
- package/modules/@apostrophecms/doc/index.js +7 -3
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +0 -1
- package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +6 -0
- package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +6 -1
- package/modules/@apostrophecms/modal/ui/apos/composables/AposFocus.js +72 -9
- package/modules/@apostrophecms/module/index.js +6 -1
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerSelectBox.vue +2 -0
- package/modules/@apostrophecms/rich-text-widget/index.js +3 -1
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapColor.vue +10 -3
- package/modules/@apostrophecms/schema/lib/addFieldTypes.js +7 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRange.js +2 -2
- package/modules/@apostrophecms/template/lib/bundlesLoader.js +18 -4
- package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +12 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +97 -11
- package/modules/@apostrophecms/ui/ui/apos/components/AposToggle.vue +5 -1
- package/modules/@apostrophecms/ui/ui/apos/composables/AposFocusTrap.js +227 -0
- package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +0 -1
- package/package.json +1 -1
- package/test/asset-external.js +183 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.11.0 (2024-12-18)
|
|
4
|
+
|
|
5
|
+
### Adds
|
|
6
|
+
|
|
7
|
+
* When validating an `area` field, warn the developer if `widgets` is not nested in `options`.
|
|
8
|
+
* Adds support for supplying CSS variable names to a color field's `presetColors` array as selectable values.
|
|
9
|
+
* Adds support for dynamic focus trap in Context menus (prop `dynamicFocus`). When set to `true`, the focusable elements are recalculated on each cycle step.
|
|
10
|
+
* Adds option to disable `tabindex` on `AposToggle` component. A new prop `disableFocus` can be set to `false` to disable the focus on the toggle button. It's enabled by default.
|
|
11
|
+
* Adds support for event on `addContextOperation`, an option `type` can now be passed and can be `modal` (default) or `event`, in this case it does not try to open a modal but emit a bus event using the action as name.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Fixes
|
|
15
|
+
|
|
16
|
+
* Focus properly Widget Editor modals when opened. Keep the previous active focus on the modal when closing the widget editor.
|
|
17
|
+
* a11y improvements for context menus.
|
|
18
|
+
* Fixes broken widget preview URL when the image is overridden (module improve) and external build module is registered.
|
|
19
|
+
* Inject dynamic custom bundle CSS when using external build module with no CSS entry point.
|
|
20
|
+
* Range field now correctly takes 0 into account.
|
|
21
|
+
* Apos style does not go through `postcss-viewport-to-container-toggle` plugin anymore to avoid UI bugs.
|
|
22
|
+
|
|
3
23
|
## 4.10.0 (2024-11-20)
|
|
4
24
|
|
|
5
25
|
### Fixes
|
|
@@ -429,11 +429,11 @@ export default {
|
|
|
429
429
|
},
|
|
430
430
|
|
|
431
431
|
attachKeyboardFocusHandler() {
|
|
432
|
-
this.$refs.wrapper
|
|
432
|
+
this.$refs.wrapper?.addEventListener('keydown', this.handleKeyboardFocus);
|
|
433
433
|
},
|
|
434
434
|
|
|
435
435
|
removeKeyboardFocusHandler() {
|
|
436
|
-
this.$refs.wrapper
|
|
436
|
+
this.$refs.wrapper?.removeEventListener('keydown', this.handleKeyboardFocus);
|
|
437
437
|
},
|
|
438
438
|
|
|
439
439
|
// Focus parent, useful for obtrusive UI
|
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
const postcssViewportToContainerToggle = require('postcss-viewport-to-container-toggle');
|
|
2
|
-
|
|
3
1
|
module.exports = (options, apos) => {
|
|
4
2
|
const postcssPlugins = [
|
|
5
|
-
...apos.asset.options.breakpointPreviewMode?.enable === true ? [
|
|
6
|
-
postcssViewportToContainerToggle({
|
|
7
|
-
modifierAttr: 'data-breakpoint-preview-mode',
|
|
8
|
-
debug: apos.asset.options.breakpointPreviewMode?.debug === true,
|
|
9
|
-
transform: apos.asset.options.breakpointPreviewMode?.transform || null
|
|
10
|
-
})
|
|
11
|
-
] : [],
|
|
12
3
|
'autoprefixer',
|
|
13
4
|
{}
|
|
14
5
|
];
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
</div>
|
|
20
20
|
<div class="apos-color__color-wrap">
|
|
21
21
|
<div
|
|
22
|
+
:data-apos-active-color="activeColor"
|
|
22
23
|
:aria-label="`Current color is ${activeColor}`"
|
|
23
24
|
class="apos-color__active-color"
|
|
24
25
|
:style="{ background: activeColor }"
|
|
@@ -77,7 +78,7 @@
|
|
|
77
78
|
:key="`!${c}`"
|
|
78
79
|
class="apos-color__presets-color"
|
|
79
80
|
:aria-label="`Color:${c}`"
|
|
80
|
-
:style="{ background: c }"
|
|
81
|
+
:style="{ background: formatCssValue(c) }"
|
|
81
82
|
@click="handlePreset(c)"
|
|
82
83
|
/>
|
|
83
84
|
<div
|
|
@@ -152,6 +153,13 @@ export default {
|
|
|
152
153
|
}
|
|
153
154
|
},
|
|
154
155
|
methods: {
|
|
156
|
+
formatCssValue(c) {
|
|
157
|
+
let value = c;
|
|
158
|
+
if (c.startsWith('--')) {
|
|
159
|
+
value = `var(${value})`;
|
|
160
|
+
}
|
|
161
|
+
return value;
|
|
162
|
+
},
|
|
155
163
|
handlePreset(c) {
|
|
156
164
|
this.colorChange(c);
|
|
157
165
|
},
|
|
@@ -124,13 +124,21 @@ export default {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
.apos-color__input {
|
|
127
|
-
width:
|
|
128
|
-
padding: 0
|
|
129
|
-
border:
|
|
127
|
+
width: 90%;
|
|
128
|
+
padding: 4px 0 3px 10%;
|
|
129
|
+
border: none;
|
|
130
|
+
font-size: var(--a-type-small);
|
|
130
131
|
outline: none;
|
|
132
|
+
box-shadow: inset 0 0 0 1px var(--a-base-5);
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
.apos-color__label {
|
|
136
|
+
display: block;
|
|
137
|
+
padding-top: 3px;
|
|
138
|
+
padding-bottom: 4px;
|
|
139
|
+
color: var(--a-text-primary);
|
|
140
|
+
font-size: var(--a-type-smaller);
|
|
141
|
+
text-align: center;
|
|
134
142
|
text-transform: capitalize;
|
|
135
143
|
}
|
|
136
144
|
</style>
|
|
@@ -73,7 +73,6 @@ export default {
|
|
|
73
73
|
this.$emit('change', param);
|
|
74
74
|
},
|
|
75
75
|
handleMouseDown(e) {
|
|
76
|
-
// this.handleChange(e, true)
|
|
77
76
|
window.addEventListener('mousemove', this.handleChange);
|
|
78
77
|
window.addEventListener('mouseup', this.handleChange);
|
|
79
78
|
window.addEventListener('mouseup', this.handleMouseUp);
|
|
@@ -76,7 +76,11 @@ export default {
|
|
|
76
76
|
},
|
|
77
77
|
update(value) {
|
|
78
78
|
this.tinyColorObj = new TinyColor(value.hsl);
|
|
79
|
-
|
|
79
|
+
if (value._cssVariable) {
|
|
80
|
+
this.next = value._cssVariable;
|
|
81
|
+
} else {
|
|
82
|
+
this.next = this.tinyColorObj.toString(this.format);
|
|
83
|
+
}
|
|
80
84
|
},
|
|
81
85
|
validate(value) {
|
|
82
86
|
if (this.field.required) {
|
|
@@ -90,7 +94,9 @@ export default {
|
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
const color = new TinyColor(value);
|
|
93
|
-
|
|
97
|
+
if (!value.startsWith('--')) {
|
|
98
|
+
return color.isValid ? false : 'Error';
|
|
99
|
+
}
|
|
94
100
|
},
|
|
95
101
|
clear() {
|
|
96
102
|
this.next = '';
|
|
@@ -19,6 +19,9 @@ function _colorChange(data, oldHue) {
|
|
|
19
19
|
color = tinycolor(data.rgba);
|
|
20
20
|
} else if (data && data.rgb) {
|
|
21
21
|
color = tinycolor(data.rgb);
|
|
22
|
+
} else if (data && typeof data === 'string' && data.startsWith('--')) {
|
|
23
|
+
color = tinycolor(getComputedStyle(document.body).getPropertyValue(data));
|
|
24
|
+
color._cssVariable = data;
|
|
22
25
|
} else {
|
|
23
26
|
color = tinycolor(data);
|
|
24
27
|
}
|
|
@@ -50,6 +53,7 @@ function _colorChange(data, oldHue) {
|
|
|
50
53
|
/* ------ */
|
|
51
54
|
|
|
52
55
|
return {
|
|
56
|
+
_cssVariable: color._cssVariable,
|
|
53
57
|
hsl,
|
|
54
58
|
hex: color.toHexString().toUpperCase(),
|
|
55
59
|
hex8: color.toHex8String().toUpperCase(),
|
|
@@ -1486,7 +1486,7 @@ module.exports = {
|
|
|
1486
1486
|
];
|
|
1487
1487
|
|
|
1488
1488
|
function validate ({
|
|
1489
|
-
action, context, label, modal, conditions, if: ifProps
|
|
1489
|
+
action, context, type = 'modal', label, modal, conditions, if: ifProps
|
|
1490
1490
|
}) {
|
|
1491
1491
|
const allowedConditions = [
|
|
1492
1492
|
'canPublish',
|
|
@@ -1503,8 +1503,12 @@ module.exports = {
|
|
|
1503
1503
|
'canShareDraft'
|
|
1504
1504
|
];
|
|
1505
1505
|
|
|
1506
|
-
if (!
|
|
1507
|
-
throw self.apos.error('invalid', '
|
|
1506
|
+
if (![ 'event', 'modal' ].includes(type)) {
|
|
1507
|
+
throw self.apos.error('invalid', '`type` option must be `modal` (default) or `event`');
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
if (!action || !context || !label || (type === 'modal' && !modal)) {
|
|
1511
|
+
throw self.apos.error('invalid', 'addContextOperation requires action, context, label and modal (if type is set to `modal` or unset) properties.');
|
|
1508
1512
|
}
|
|
1509
1513
|
|
|
1510
1514
|
if (
|
|
@@ -24,7 +24,6 @@ import AposDocContextMenuLogic from 'Modules/@apostrophecms/doc-type/logic/AposD
|
|
|
24
24
|
export default {
|
|
25
25
|
name: 'AposDocContextMenu',
|
|
26
26
|
mixins: [ AposDocContextMenuLogic ],
|
|
27
|
-
// Satisfy linting.
|
|
28
27
|
emits: [ 'menu-open', 'menu-close', 'close' ]
|
|
29
28
|
};
|
|
30
29
|
</script>
|
|
@@ -168,6 +168,7 @@ export default {
|
|
|
168
168
|
}
|
|
169
169
|
] : [])
|
|
170
170
|
];
|
|
171
|
+
|
|
171
172
|
return menu;
|
|
172
173
|
},
|
|
173
174
|
customMenusByContext() {
|
|
@@ -394,6 +395,7 @@ export default {
|
|
|
394
395
|
this.customAction(this.context, operation);
|
|
395
396
|
return;
|
|
396
397
|
}
|
|
398
|
+
|
|
397
399
|
this[action](this.context);
|
|
398
400
|
},
|
|
399
401
|
async edit(doc) {
|
|
@@ -449,6 +451,10 @@ export default {
|
|
|
449
451
|
...docProps(doc),
|
|
450
452
|
...operation.props
|
|
451
453
|
};
|
|
454
|
+
if (operation.type === 'event') {
|
|
455
|
+
apos.bus.$emit(operation.action, props);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
452
458
|
await apos.modal.execute(operation.modal, props);
|
|
453
459
|
function docProps(doc) {
|
|
454
460
|
return Object.fromEntries(Object.entries(operation.docProps || {}).map(([ key, value ]) => {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
aria-modal="true"
|
|
14
14
|
:aria-labelledby="props.modalData.id"
|
|
15
15
|
data-apos-modal
|
|
16
|
-
@focus.capture="
|
|
16
|
+
@focus.capture="captureFocus"
|
|
17
17
|
@esc="close"
|
|
18
18
|
@keydown.tab="onTab"
|
|
19
19
|
>
|
|
@@ -264,6 +264,11 @@ function onLeave() {
|
|
|
264
264
|
emit('no-modal');
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
function captureFocus(e) {
|
|
268
|
+
storeFocusedElement(e);
|
|
269
|
+
store.updateModalData(props.modalData.id, { focusedElement: e.target });
|
|
270
|
+
}
|
|
271
|
+
|
|
267
272
|
async function trapFocus() {
|
|
268
273
|
if (modalEl?.value) {
|
|
269
274
|
const elementSelectors = [
|
|
@@ -10,6 +10,9 @@ export function useAposFocus() {
|
|
|
10
10
|
return {
|
|
11
11
|
elementsToFocus,
|
|
12
12
|
focusedElement,
|
|
13
|
+
activeModal: modalStore.activeModal,
|
|
14
|
+
activeModalElementsToFocus: modalStore.activeModal?.elementsToFocus,
|
|
15
|
+
activeModalFocusedElement: modalStore.activeModal?.focusedElement,
|
|
13
16
|
cycleElementsToFocus,
|
|
14
17
|
focusLastModalFocusedElement,
|
|
15
18
|
storeFocusedElement,
|
|
@@ -26,7 +29,18 @@ export function useAposFocus() {
|
|
|
26
29
|
// `cycleElementsToFocus` listeners relies on this dynamic list which has the advantage of
|
|
27
30
|
// taking new or less elements to focus, after an update has happened inside a modal,
|
|
28
31
|
// like an XHR call to get the pieces list in the AposDocsManager modal, for instance.
|
|
29
|
-
|
|
32
|
+
// If the fnFocus argument is provided, it will be called with the event and
|
|
33
|
+
// the element to focus. Otherwise, the default behavior is to focus the element
|
|
34
|
+
// and prevent the default event behavior.
|
|
35
|
+
/**
|
|
36
|
+
* @param {KeyboardEvent} e event
|
|
37
|
+
* @param {HTMLElement[]} elements
|
|
38
|
+
* @param {
|
|
39
|
+
* (event: KeyboardEvent, element: HTMLElement) => void
|
|
40
|
+
* } [fnFocus] optional function to focus the element
|
|
41
|
+
* @returns {void}
|
|
42
|
+
*/
|
|
43
|
+
function cycleElementsToFocus(e, elements, fnFocus) {
|
|
30
44
|
const elems = elements || elementsToFocus.value;
|
|
31
45
|
if (!elems.length) {
|
|
32
46
|
return;
|
|
@@ -37,25 +51,74 @@ export function useAposFocus() {
|
|
|
37
51
|
return;
|
|
38
52
|
}
|
|
39
53
|
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
let firstElementToFocus = elems.at(0);
|
|
55
|
+
let lastElementToFocus = elems.at(-1);
|
|
56
|
+
|
|
57
|
+
// Take into account radio inputs with the same name, the
|
|
58
|
+
// browser will cycle through them as a group, stepping on
|
|
59
|
+
// the active one per stack.
|
|
60
|
+
const firstElementRadioStack = getInputRadioStack(firstElementToFocus, elems);
|
|
61
|
+
const lastElementRadioStack = getInputRadioStack(lastElementToFocus, elems);
|
|
62
|
+
firstElementToFocus = getInputCheckedOrCurrent(firstElementToFocus, firstElementRadioStack);
|
|
63
|
+
lastElementToFocus = getInputCheckedOrCurrent(lastElementToFocus, lastElementRadioStack);
|
|
64
|
+
|
|
65
|
+
const focus = fnFocus || ((ev, el) => {
|
|
66
|
+
el.focus();
|
|
67
|
+
ev.preventDefault();
|
|
68
|
+
});
|
|
42
69
|
|
|
43
70
|
// If shift key pressed for shift + tab combination
|
|
44
71
|
if (e.shiftKey) {
|
|
45
|
-
if (document.activeElement === firstElementToFocus
|
|
72
|
+
if (document.activeElement === firstElementToFocus ||
|
|
73
|
+
firstElementRadioStack.includes(document.activeElement)
|
|
74
|
+
) {
|
|
46
75
|
// Add focus for the last focusable element
|
|
47
|
-
|
|
48
|
-
e.preventDefault();
|
|
76
|
+
focus(e, lastElementToFocus);
|
|
49
77
|
}
|
|
50
78
|
return;
|
|
51
79
|
}
|
|
52
80
|
|
|
53
81
|
// If tab key is pressed
|
|
54
|
-
if (document.activeElement === lastElementToFocus
|
|
82
|
+
if (document.activeElement === lastElementToFocus ||
|
|
83
|
+
lastElementRadioStack.includes(document.activeElement)
|
|
84
|
+
) {
|
|
55
85
|
// Add focus for the first focusable element
|
|
56
|
-
|
|
57
|
-
|
|
86
|
+
focus(e, firstElementToFocus);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Returns an array of radio inputs with the same name attribute
|
|
92
|
+
* as the current element. If the current element is not a radio input,
|
|
93
|
+
* an empty array is returned.
|
|
94
|
+
*
|
|
95
|
+
* @param {HTMLElement} currentElement
|
|
96
|
+
* @param {HTMLElement[]} elements
|
|
97
|
+
* @returns {HTMLElement[]}
|
|
98
|
+
*/
|
|
99
|
+
function getInputRadioStack(currentElement, elements) {
|
|
100
|
+
return currentElement.getAttribute('type') === 'radio'
|
|
101
|
+
? elements.filter(
|
|
102
|
+
e => (e.getAttribute('type') === 'radio' &&
|
|
103
|
+
e.getAttribute('name') === currentElement.getAttribute('name'))
|
|
104
|
+
)
|
|
105
|
+
: [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
*
|
|
110
|
+
* @param {HTMLElement} currentElement
|
|
111
|
+
* @param {HTMLElement[]} elements
|
|
112
|
+
* @returns
|
|
113
|
+
*/
|
|
114
|
+
function getInputCheckedOrCurrent(currentElement, elements = []) {
|
|
115
|
+
const checked = elements.find(el => (el.hasAttribute('checked')));
|
|
116
|
+
|
|
117
|
+
if (checked) {
|
|
118
|
+
return checked;
|
|
58
119
|
}
|
|
120
|
+
|
|
121
|
+
return currentElement;
|
|
59
122
|
}
|
|
60
123
|
|
|
61
124
|
// Focus the last focused element from the last modal.
|
|
@@ -818,7 +818,8 @@ module.exports = {
|
|
|
818
818
|
let urlOption = self.options[`${name}Url`];
|
|
819
819
|
const imageOption = self.options[`${name}Image`];
|
|
820
820
|
if (!urlOption) {
|
|
821
|
-
|
|
821
|
+
// Webpack and the legacy asset pipeline
|
|
822
|
+
if (imageOption && !self.apos.asset.hasBuildModule()) {
|
|
822
823
|
const chain = [ ...self.__meta.chain ].reverse();
|
|
823
824
|
for (const entry of chain) {
|
|
824
825
|
const path = `${entry.dirname}/public/${name}.${imageOption}`;
|
|
@@ -828,6 +829,10 @@ module.exports = {
|
|
|
828
829
|
}
|
|
829
830
|
}
|
|
830
831
|
}
|
|
832
|
+
// The new external module asset pipeline
|
|
833
|
+
if (imageOption && self.apos.asset.hasBuildModule()) {
|
|
834
|
+
urlOption = `/modules/${self.__meta.name}/${name}.${imageOption}`;
|
|
835
|
+
}
|
|
831
836
|
}
|
|
832
837
|
if (urlOption && urlOption.startsWith('/modules')) {
|
|
833
838
|
urlOption = self.apos.asset.url(urlOption);
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
:label="selectBoxMessageButton"
|
|
18
18
|
class="apos-select-box__select-all"
|
|
19
19
|
text-color="var(--a-primary)"
|
|
20
|
+
:disabled="!showSelectAll"
|
|
20
21
|
@click="$emit('select-all')"
|
|
21
22
|
/>
|
|
22
23
|
<AposButton
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
label="apostrophe:clearSelection"
|
|
27
28
|
class="apos-select-box__select-all"
|
|
28
29
|
text-color="var(--a-primary)"
|
|
30
|
+
:disabled="!showSelectAll"
|
|
29
31
|
@click="clearSelection"
|
|
30
32
|
/>
|
|
31
33
|
</p>
|
|
@@ -617,7 +617,9 @@ module.exports = {
|
|
|
617
617
|
// HSL colors
|
|
618
618
|
/^hsl\(\s*\d{1,3}(?:deg)?\s*,\s*\d{1,3}%\s*,\s*\d{1,3}%\s*\)$/,
|
|
619
619
|
// HSLA colors
|
|
620
|
-
/^hsla\(\s*\d{1,3}(?:deg)?\s*,\s*\d{1,3}%\s*,\s*\d{1,3}%\s*,\s*(?:0?\.\d+|1(?:\.0)?)\s*\)
|
|
620
|
+
/^hsla\(\s*\d{1,3}(?:deg)?\s*,\s*\d{1,3}%\s*,\s*\d{1,3}%\s*,\s*(?:0?\.\d+|1(?:\.0)?)\s*\)$/,
|
|
621
|
+
// CSS Variable value
|
|
622
|
+
/^var\(--[a-zA-Z0-9-]+\)$/
|
|
621
623
|
]
|
|
622
624
|
}
|
|
623
625
|
}
|
|
@@ -177,9 +177,16 @@ export default defineComponent({
|
|
|
177
177
|
};
|
|
178
178
|
|
|
179
179
|
const update = (value) => {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
180
|
+
let color;
|
|
181
|
+
if (value._cssVariable) {
|
|
182
|
+
next.value = value._cssVariable;
|
|
183
|
+
color = `var(${next.value})`;
|
|
184
|
+
} else {
|
|
185
|
+
tinyColorObj.value = new TinyColor(value.hsl);
|
|
186
|
+
next.value = tinyColorObj.value.toString(format.value);
|
|
187
|
+
color = next.value;
|
|
188
|
+
}
|
|
189
|
+
props.editor.chain().focus().setColor(color).run();
|
|
183
190
|
indicatorColor.value = next.value;
|
|
184
191
|
};
|
|
185
192
|
|
|
@@ -56,6 +56,13 @@ module.exports = (self) => {
|
|
|
56
56
|
return _.isEqual(oneArea, twoArea);
|
|
57
57
|
},
|
|
58
58
|
validate: function (field, options, warn, fail) {
|
|
59
|
+
if (field.widgets) {
|
|
60
|
+
warn(stripIndents`
|
|
61
|
+
Remember to nest "widgets" inside "options" when configuring an area field.
|
|
62
|
+
|
|
63
|
+
Otherwise, "widgets" has no effect.
|
|
64
|
+
`);
|
|
65
|
+
}
|
|
59
66
|
let widgets = (field.options && field.options.widgets) || {};
|
|
60
67
|
|
|
61
68
|
if (field.options && field.options.groups) {
|
|
@@ -32,7 +32,7 @@ export default {
|
|
|
32
32
|
// The range spec defaults to a value of midway between the min and max
|
|
33
33
|
// Example: a range with an unset value and a min of 0 and max of 100 will become 50
|
|
34
34
|
// This does not allow ranges to go unset :(
|
|
35
|
-
if (!this.next) {
|
|
35
|
+
if (!this.next && this.next !== 0) {
|
|
36
36
|
this.unset();
|
|
37
37
|
}
|
|
38
38
|
},
|
|
@@ -47,7 +47,7 @@ export default {
|
|
|
47
47
|
},
|
|
48
48
|
validate(value) {
|
|
49
49
|
if (this.field.required) {
|
|
50
|
-
if (!value) {
|
|
50
|
+
if (!value && value !== 0) {
|
|
51
51
|
return 'required';
|
|
52
52
|
}
|
|
53
53
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
const { stripIndent } = require('common-tags');
|
|
2
2
|
|
|
3
3
|
module.exports = (self) => {
|
|
4
|
+
// FIXME: This entire function should be separated for external build modules.
|
|
5
|
+
// Use the next opportunity to clean up and let the legacy system be and
|
|
6
|
+
// introduce a new one for external build modules (e.g. `insertBundlesMarkupByManifest`).
|
|
7
|
+
// The only check for external build modules should be at the very top of
|
|
8
|
+
// `insertBundlesMarkup` function, resulting in a call to our new
|
|
9
|
+
// function.
|
|
4
10
|
function insertBundlesMarkup({
|
|
5
11
|
page = {},
|
|
6
12
|
template = '',
|
|
@@ -30,7 +36,7 @@ module.exports = (self) => {
|
|
|
30
36
|
});
|
|
31
37
|
|
|
32
38
|
if (scene === 'apos') {
|
|
33
|
-
return loadAllBundles({
|
|
39
|
+
return loadAllBundles(self, {
|
|
34
40
|
content,
|
|
35
41
|
scriptsPlaceholder,
|
|
36
42
|
stylesheetsPlaceholder,
|
|
@@ -75,6 +81,10 @@ module.exports = (self) => {
|
|
|
75
81
|
};
|
|
76
82
|
}, widgetsBundles);
|
|
77
83
|
|
|
84
|
+
const cssExtraBundles = self.apos.asset.hasBuildModule()
|
|
85
|
+
? Array.from(new Set([ ...extraBundles.js, ...extraBundles.css ]))
|
|
86
|
+
: extraBundles.css;
|
|
87
|
+
|
|
78
88
|
const { jsBundles, cssBundles } = Object.entries(configs)
|
|
79
89
|
.reduce((acc, [ name, { templates } ]) => {
|
|
80
90
|
if (templates && !templates.includes(templateType)) {
|
|
@@ -90,7 +100,7 @@ module.exports = (self) => {
|
|
|
90
100
|
});
|
|
91
101
|
|
|
92
102
|
const cssMarkup = stylesheetsPlaceholder &&
|
|
93
|
-
|
|
103
|
+
cssExtraBundles.includes(name) &&
|
|
94
104
|
renderMarkup({
|
|
95
105
|
fileName: name,
|
|
96
106
|
ext: 'css'
|
|
@@ -175,7 +185,7 @@ function renderBundleMarkupByManifest(self, modulePreload) {
|
|
|
175
185
|
};
|
|
176
186
|
}
|
|
177
187
|
|
|
178
|
-
function loadAllBundles({
|
|
188
|
+
function loadAllBundles(self, {
|
|
179
189
|
content,
|
|
180
190
|
extraBundles,
|
|
181
191
|
scriptsPlaceholder,
|
|
@@ -199,11 +209,15 @@ function loadAllBundles({
|
|
|
199
209
|
`;
|
|
200
210
|
};
|
|
201
211
|
|
|
212
|
+
const cssExtraBundles = self.apos.asset.hasBuildModule()
|
|
213
|
+
? Array.from(new Set([ ...extraBundles.js, ...extraBundles.css ]))
|
|
214
|
+
: extraBundles.css;
|
|
215
|
+
|
|
202
216
|
const jsBundles = extraBundles.js.reduce(
|
|
203
217
|
(acc, bundle) => reduceToMarkup(acc, bundle, 'js'), jsMainBundle
|
|
204
218
|
);
|
|
205
219
|
|
|
206
|
-
const cssBundles =
|
|
220
|
+
const cssBundles = cssExtraBundles.reduce(
|
|
207
221
|
(acc, bundle) => reduceToMarkup(acc, bundle, 'css'), cssMainBundle
|
|
208
222
|
);
|
|
209
223
|
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
v-bind="attrs"
|
|
10
10
|
:is="href ? 'a' : 'button'"
|
|
11
11
|
:id="attrs.id ? attrs.id : id"
|
|
12
|
+
ref="buttonTrigger"
|
|
12
13
|
:target="target"
|
|
13
14
|
:href="href"
|
|
14
15
|
class="apos-button"
|
|
@@ -164,8 +165,15 @@ export default {
|
|
|
164
165
|
if (this.type === 'color') {
|
|
165
166
|
// if color exists, use it
|
|
166
167
|
if (this.color) {
|
|
168
|
+
|
|
169
|
+
let color = this.color;
|
|
170
|
+
|
|
171
|
+
if (color.startsWith('--')) {
|
|
172
|
+
color = `var(${color})`;
|
|
173
|
+
}
|
|
174
|
+
|
|
167
175
|
return {
|
|
168
|
-
backgroundColor:
|
|
176
|
+
backgroundColor: color
|
|
169
177
|
};
|
|
170
178
|
// if not provide a default placeholder
|
|
171
179
|
} else {
|
|
@@ -235,6 +243,9 @@ export default {
|
|
|
235
243
|
methods: {
|
|
236
244
|
click($event) {
|
|
237
245
|
this.$emit('click', $event);
|
|
246
|
+
},
|
|
247
|
+
focus() {
|
|
248
|
+
(this.$refs.buttonTrigger?.$el ?? this.$refs.buttonTrigger)?.focus();
|
|
238
249
|
}
|
|
239
250
|
}
|
|
240
251
|
};
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
2
|
+
<section
|
|
3
|
+
ref="contextMenuRef"
|
|
4
|
+
class="apos-context-menu"
|
|
5
|
+
@keydown.tab="onTab"
|
|
6
|
+
>
|
|
3
7
|
<slot name="prebutton" />
|
|
4
8
|
<div
|
|
5
9
|
ref="dropdown"
|
|
@@ -7,7 +11,7 @@
|
|
|
7
11
|
>
|
|
8
12
|
<AposButton
|
|
9
13
|
v-bind="button"
|
|
10
|
-
ref="
|
|
14
|
+
ref="dropdownButton"
|
|
11
15
|
class="apos-context-menu__btn"
|
|
12
16
|
role="button"
|
|
13
17
|
:data-apos-test="identifier"
|
|
@@ -25,6 +29,7 @@
|
|
|
25
29
|
v-if="isOpen"
|
|
26
30
|
ref="dropdownContent"
|
|
27
31
|
v-click-outside-element="hide"
|
|
32
|
+
:data-apos-test="isRendered ? 'context-menu-content' : null"
|
|
28
33
|
class="apos-context-menu__dropdown-content"
|
|
29
34
|
:class="popoverClass"
|
|
30
35
|
data-apos-menu
|
|
@@ -44,7 +49,7 @@
|
|
|
44
49
|
</AposContextMenuDialog>
|
|
45
50
|
</div>
|
|
46
51
|
</div>
|
|
47
|
-
</
|
|
52
|
+
</section>
|
|
48
53
|
</template>
|
|
49
54
|
|
|
50
55
|
<script setup>
|
|
@@ -54,9 +59,11 @@ import {
|
|
|
54
59
|
import {
|
|
55
60
|
computePosition, offset, shift, flip, arrow
|
|
56
61
|
} from '@floating-ui/dom';
|
|
57
|
-
import { useAposTheme } from 'Modules/@apostrophecms/ui/composables/AposTheme';
|
|
58
62
|
import { createId } from '@paralleldrive/cuid2';
|
|
59
63
|
|
|
64
|
+
import { useAposTheme } from '../composables/AposTheme.js';
|
|
65
|
+
import { useFocusTrap } from '../composables/AposFocusTrap.js';
|
|
66
|
+
|
|
60
67
|
const props = defineProps({
|
|
61
68
|
identifier: {
|
|
62
69
|
type: String,
|
|
@@ -122,15 +129,34 @@ const props = defineProps({
|
|
|
122
129
|
activeItem: {
|
|
123
130
|
type: String,
|
|
124
131
|
default: null
|
|
132
|
+
},
|
|
133
|
+
trapFocus: {
|
|
134
|
+
type: Boolean,
|
|
135
|
+
default: true
|
|
136
|
+
},
|
|
137
|
+
// When set to true, the elements to focus on will be re-queried
|
|
138
|
+
// on everu Tab key press. Use this with caution, as it's a performance
|
|
139
|
+
// hit. Only use this if you have a context menu with
|
|
140
|
+
// dynamically changing (e.g. AposToggle item enables another item) items.
|
|
141
|
+
dynamicFocus: {
|
|
142
|
+
type: Boolean,
|
|
143
|
+
default: false
|
|
125
144
|
}
|
|
126
145
|
});
|
|
127
146
|
|
|
128
147
|
const emit = defineEmits([ 'open', 'close', 'item-clicked' ]);
|
|
129
148
|
|
|
130
149
|
const isOpen = ref(false);
|
|
150
|
+
const isRendered = ref(false);
|
|
131
151
|
const placement = ref(props.menuPlacement);
|
|
132
152
|
const event = ref(null);
|
|
153
|
+
/** @type {import('vue').Ref<HTMLElement | null>}} */
|
|
154
|
+
const contextMenuRef = ref(null);
|
|
155
|
+
/** @type {import('vue').Ref<HTMLElement | null>}} */
|
|
133
156
|
const dropdown = ref(null);
|
|
157
|
+
/** @type {import('vue').Ref<import('vue').ComponentPublicInstance | null>} */
|
|
158
|
+
const dropdownButton = ref(null);
|
|
159
|
+
/** @type {import('vue').Ref<HTMLElement | null>} */
|
|
134
160
|
const dropdownContent = ref(null);
|
|
135
161
|
const dropdownContentStyle = ref({});
|
|
136
162
|
const arrowEl = ref(null);
|
|
@@ -138,6 +164,17 @@ const iconToCenterTo = ref(null);
|
|
|
138
164
|
const menuOffset = getMenuOffset();
|
|
139
165
|
const otherMenuOpened = ref(false);
|
|
140
166
|
|
|
167
|
+
const {
|
|
168
|
+
onTab, runTrap, hasRunningTrap, resetTrap
|
|
169
|
+
} = useFocusTrap({
|
|
170
|
+
withPriority: true,
|
|
171
|
+
refreshOnCycle: props.dynamicFocus
|
|
172
|
+
// If enabled, the dropdown gets closed when the focus leaves
|
|
173
|
+
// the context menu.
|
|
174
|
+
// triggerRef: dropdownButton,
|
|
175
|
+
// onExit: hide
|
|
176
|
+
});
|
|
177
|
+
|
|
141
178
|
defineExpose({
|
|
142
179
|
hide,
|
|
143
180
|
setDropdownPosition
|
|
@@ -170,19 +207,28 @@ const buttonState = computed(() => {
|
|
|
170
207
|
return isOpen.value ? [ 'active' ] : null;
|
|
171
208
|
});
|
|
172
209
|
|
|
173
|
-
watch(isOpen, (newVal) => {
|
|
210
|
+
watch(isOpen, async (newVal) => {
|
|
174
211
|
emit(newVal ? 'open' : 'close', event.value);
|
|
175
212
|
if (newVal) {
|
|
176
213
|
setDropdownPosition();
|
|
177
214
|
window.addEventListener('resize', setDropdownPosition);
|
|
178
215
|
window.addEventListener('scroll', setDropdownPosition);
|
|
179
|
-
|
|
180
|
-
|
|
216
|
+
contextMenuRef.value?.addEventListener('keydown', handleKeyboard);
|
|
217
|
+
if (props.trapFocus && !hasRunningTrap.value) {
|
|
218
|
+
await runTrap(dropdownContent);
|
|
219
|
+
}
|
|
220
|
+
if (!props.trapFocus) {
|
|
221
|
+
dropdownContent.value.querySelector('[tabindex]')?.focus();
|
|
222
|
+
}
|
|
223
|
+
isRendered.value = true;
|
|
181
224
|
} else {
|
|
225
|
+
if (props.trapFocus) {
|
|
226
|
+
resetTrap();
|
|
227
|
+
}
|
|
182
228
|
window.removeEventListener('resize', setDropdownPosition);
|
|
183
229
|
window.removeEventListener('scroll', setDropdownPosition);
|
|
184
|
-
|
|
185
|
-
if (!otherMenuOpened.value) {
|
|
230
|
+
contextMenuRef.value?.addEventListener('keydown', handleKeyboard);
|
|
231
|
+
if (!otherMenuOpened.value && !props.trapFocus) {
|
|
186
232
|
dropdown.value.querySelector('[tabindex]').focus();
|
|
187
233
|
}
|
|
188
234
|
}
|
|
@@ -276,11 +322,51 @@ async function setDropdownPosition() {
|
|
|
276
322
|
});
|
|
277
323
|
}
|
|
278
324
|
|
|
325
|
+
const ignoreInputTypes = [
|
|
326
|
+
'text',
|
|
327
|
+
'password',
|
|
328
|
+
'email',
|
|
329
|
+
'file',
|
|
330
|
+
'number',
|
|
331
|
+
'search',
|
|
332
|
+
'tel',
|
|
333
|
+
'url',
|
|
334
|
+
'date',
|
|
335
|
+
'time',
|
|
336
|
+
'datetime-local',
|
|
337
|
+
'month',
|
|
338
|
+
'search',
|
|
339
|
+
'week'
|
|
340
|
+
];
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* @param {KeyboardEvent} event
|
|
344
|
+
*/
|
|
279
345
|
function handleKeyboard(event) {
|
|
280
|
-
if (event.key
|
|
346
|
+
if (event.key !== 'Escape' || !isOpen.value) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
/** @type {HTMLElement} */
|
|
350
|
+
const target = event.target;
|
|
351
|
+
|
|
352
|
+
// If inside of an input or textarea, don't close the dropdown
|
|
353
|
+
// and don't allow other event listeners to close it either (e.g. modals)
|
|
354
|
+
if (
|
|
355
|
+
target?.nodeName?.toLowerCase() === 'textarea' ||
|
|
356
|
+
(target?.nodeName?.toLowerCase() === 'input' &&
|
|
357
|
+
ignoreInputTypes.includes(target.getAttribute('type'))
|
|
358
|
+
)
|
|
359
|
+
) {
|
|
281
360
|
event.stopImmediatePropagation();
|
|
282
|
-
|
|
361
|
+
return;
|
|
283
362
|
}
|
|
363
|
+
|
|
364
|
+
dropdownButton.value?.focus
|
|
365
|
+
? dropdownButton.value.focus()
|
|
366
|
+
: dropdownButton.value?.$el?.focus();
|
|
367
|
+
|
|
368
|
+
event.stopImmediatePropagation();
|
|
369
|
+
hide();
|
|
284
370
|
}
|
|
285
371
|
</script>
|
|
286
372
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<div class="apos-toggle__container">
|
|
3
3
|
<div
|
|
4
4
|
class="apos-toggle__slider"
|
|
5
|
-
tabindex="0"
|
|
5
|
+
:tabindex="disableFocus ? null : '0'"
|
|
6
6
|
:class="{'apos-toggle__slider--activated': !modelValue}"
|
|
7
7
|
@click="$emit('toggle')"
|
|
8
8
|
@keydown.stop.space="$emit('toggle')"
|
|
@@ -18,6 +18,10 @@ export default {
|
|
|
18
18
|
modelValue: {
|
|
19
19
|
type: Boolean,
|
|
20
20
|
required: true
|
|
21
|
+
},
|
|
22
|
+
disableFocus: {
|
|
23
|
+
type: Boolean,
|
|
24
|
+
default: false
|
|
21
25
|
}
|
|
22
26
|
},
|
|
23
27
|
emits: [ 'toggle' ],
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { useAposFocus } from 'Modules/@apostrophecms/modal/composables/AposFocus';
|
|
2
|
+
import {
|
|
3
|
+
computed, ref, unref, nextTick
|
|
4
|
+
} from 'vue';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Handle focus trapping inside a modal or any other element.
|
|
8
|
+
*
|
|
9
|
+
* Options:
|
|
10
|
+
* - `retries`: Number of retries to focus (trap) the first element in the given
|
|
11
|
+
* container. Default is 3.
|
|
12
|
+
* - `refreshOnCycle`: If true, the elements to focus will be refreshed (query)
|
|
13
|
+
* on each cycle. Default is false.
|
|
14
|
+
* - `withPriority`: If true, 'data-apos-focus-priority' attribute will be used
|
|
15
|
+
* to find the first element to focus. Default is true.
|
|
16
|
+
* - `triggerRef`: (optional) A ref to the element that will trigger the focus trap.
|
|
17
|
+
* It's used as a focus target when exiting the current element focusable elements.
|
|
18
|
+
* If boolean `true` is passed, the active modal focused element will be used.
|
|
19
|
+
* - `onExit`: (optional) A callback to be called when exiting the focus trap.
|
|
20
|
+
*
|
|
21
|
+
* @param {{
|
|
22
|
+
* retries?: number;
|
|
23
|
+
* refreshOnCycle?: boolean;
|
|
24
|
+
* withPriority?: boolean;
|
|
25
|
+
* triggerRef?: import('vue').Ref<HTMLElement | import('vue').ComponentPublicInstance>
|
|
26
|
+
* | HTMLElement | boolean;
|
|
27
|
+
* onExit?: () => void;
|
|
28
|
+
* }} options
|
|
29
|
+
* @returns {{
|
|
30
|
+
* runTrap: (containerRef: import('vue').Ref<HTMLElement> | HTMLElement) => Promise<void>;
|
|
31
|
+
* hasRunningTrap: import('vue').ComputedRef<boolean>;
|
|
32
|
+
* stopTrap: () => void;
|
|
33
|
+
* resetTrap: () => void;
|
|
34
|
+
* onTab: (event: KeyboardEvent) => void;
|
|
35
|
+
* }}
|
|
36
|
+
*/
|
|
37
|
+
export function useFocusTrap({
|
|
38
|
+
triggerRef,
|
|
39
|
+
refreshOnCycle = false,
|
|
40
|
+
onExit = () => {},
|
|
41
|
+
retries = 3,
|
|
42
|
+
withPriority = true
|
|
43
|
+
}) {
|
|
44
|
+
const {
|
|
45
|
+
activeModalFocusedElement,
|
|
46
|
+
findPriorityElementOrFirst,
|
|
47
|
+
cycleElementsToFocus: parentCycleElementsToFocus,
|
|
48
|
+
focusElement
|
|
49
|
+
} = useAposFocus();
|
|
50
|
+
|
|
51
|
+
const shouldRun = ref(false);
|
|
52
|
+
const isRunning = ref(false);
|
|
53
|
+
const currentRetries = ref(0);
|
|
54
|
+
const rootRef = ref(null);
|
|
55
|
+
const elementsToFocus = ref([]);
|
|
56
|
+
const hasRunningTrap = computed(() => {
|
|
57
|
+
return isRunning.value;
|
|
58
|
+
});
|
|
59
|
+
const triggerRefElement = computed(() => {
|
|
60
|
+
const value = unref(triggerRef);
|
|
61
|
+
if (value === true) {
|
|
62
|
+
return activeModalFocusedElement;
|
|
63
|
+
}
|
|
64
|
+
if (value) {
|
|
65
|
+
const element = value.$el || value;
|
|
66
|
+
if (element instanceof HTMLElement) {
|
|
67
|
+
return element.hasAttribute('tabindex')
|
|
68
|
+
? element
|
|
69
|
+
: (element.querySelector('[tabindex]') || element);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const selectors = [
|
|
76
|
+
'[tabindex]',
|
|
77
|
+
'[href]',
|
|
78
|
+
'input',
|
|
79
|
+
'select',
|
|
80
|
+
'textarea',
|
|
81
|
+
'button',
|
|
82
|
+
'[data-apos-focus-priority]'
|
|
83
|
+
];
|
|
84
|
+
const selector = selectors
|
|
85
|
+
.map(addExcludingAttributes)
|
|
86
|
+
.join(', ');
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
runTrap: run,
|
|
90
|
+
hasRunningTrap,
|
|
91
|
+
stopTrap: stop,
|
|
92
|
+
resetTrap: reset,
|
|
93
|
+
onTab: cycle
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The internal implementation of the focus trap.
|
|
98
|
+
*/
|
|
99
|
+
async function trapFocus(containerRef) {
|
|
100
|
+
if (!unref(containerRef) || !shouldRun.value) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const elements = [ ...unref(containerRef).querySelectorAll(selector) ];
|
|
104
|
+
const firstElementToFocus = unref(withPriority)
|
|
105
|
+
? findPriorityElementOrFirst(elements)
|
|
106
|
+
: elements[0];
|
|
107
|
+
const isPriorityElement = unref(withPriority)
|
|
108
|
+
? firstElementToFocus?.hasAttribute('data-apos-focus-priority')
|
|
109
|
+
: firstElementToFocus;
|
|
110
|
+
|
|
111
|
+
if (!isPriorityElement && unref(retries) > currentRetries.value) {
|
|
112
|
+
currentRetries.value++;
|
|
113
|
+
await wait(20);
|
|
114
|
+
return trapFocus(containerRef);
|
|
115
|
+
}
|
|
116
|
+
await nextTick();
|
|
117
|
+
|
|
118
|
+
if (shouldRun.value) {
|
|
119
|
+
focusElement(findChecked(firstElementToFocus, elements));
|
|
120
|
+
elementsToFocus.value = elements;
|
|
121
|
+
rootRef.value = unref(containerRef);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Run the focus trap
|
|
127
|
+
*/
|
|
128
|
+
async function run(containerRef) {
|
|
129
|
+
if (isRunning.value) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
shouldRun.value = true;
|
|
133
|
+
isRunning.value = true;
|
|
134
|
+
await trapFocus(containerRef);
|
|
135
|
+
isRunning.value = false;
|
|
136
|
+
shouldRun.value = false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Stop the focus trap
|
|
141
|
+
*/
|
|
142
|
+
function stop() {
|
|
143
|
+
shouldRun.value = false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Reset the focus trap
|
|
148
|
+
*/
|
|
149
|
+
function reset() {
|
|
150
|
+
shouldRun.value = false;
|
|
151
|
+
isRunning.value = false;
|
|
152
|
+
currentRetries.value = 0;
|
|
153
|
+
elementsToFocus.value = [];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Cycle through the elements to focus in the container element.
|
|
158
|
+
* If no modal is active, it will use the natural focusable elements.
|
|
159
|
+
*
|
|
160
|
+
* @param {KeyboardEvent} event
|
|
161
|
+
*/
|
|
162
|
+
function cycle(event) {
|
|
163
|
+
if (refreshOnCycle && rootRef.value) {
|
|
164
|
+
const elements = [ ...unref(rootRef).querySelectorAll(selector) ];
|
|
165
|
+
elementsToFocus.value = elements;
|
|
166
|
+
}
|
|
167
|
+
const elements = unref(elementsToFocus);
|
|
168
|
+
parentCycleElementsToFocus(event, elements, focus);
|
|
169
|
+
|
|
170
|
+
// Keep the if branches for better readability and future changes.
|
|
171
|
+
function focus(ev, element) {
|
|
172
|
+
let toFocusEl;
|
|
173
|
+
const currentFocused = triggerRefElement.value;
|
|
174
|
+
|
|
175
|
+
// If no trigger element is found, fallback to the original behavior.
|
|
176
|
+
if (!currentFocused) {
|
|
177
|
+
element.focus();
|
|
178
|
+
ev.preventDefault();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// We did a full cycle and are returning back to the first element.
|
|
182
|
+
// We don't want that, but to exit the cycle and continue to the next
|
|
183
|
+
// modal element to focus or the next natural focusable element (if
|
|
184
|
+
// not inside a modal).
|
|
185
|
+
if (element === elements[0]) {
|
|
186
|
+
toFocusEl = currentFocused;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// We are shift + tabbing from the first element. We want to focus the
|
|
190
|
+
// modal last focused element if available.
|
|
191
|
+
if (element === elements.at(-1)) {
|
|
192
|
+
toFocusEl = currentFocused;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (toFocusEl) {
|
|
196
|
+
toFocusEl.focus();
|
|
197
|
+
ev.preventDefault();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// The focus handler is called ONLY when we are exiting the container
|
|
201
|
+
// element. No matter if we find a focusable element or not, we should
|
|
202
|
+
// call the onExit callback - the focus should be outside the container
|
|
203
|
+
// element.
|
|
204
|
+
onExit();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
function addExcludingAttributes(selector) {
|
|
210
|
+
return `${selector}:not([tabindex="-1"]):not([disabled]):not([type="hidden"]):not([aria-hidden])`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function findChecked(element, elements) {
|
|
214
|
+
if (element?.getAttribute('type') === 'radio') {
|
|
215
|
+
return elements.find(
|
|
216
|
+
el => (el.getAttribute('type') === 'radio' &&
|
|
217
|
+
el.getAttribute('name') === element.getAttribute('name') &&
|
|
218
|
+
el.hasAttribute('checked'))
|
|
219
|
+
) || element;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return element;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function wait(ms) {
|
|
226
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
227
|
+
}
|
package/package.json
CHANGED
package/test/asset-external.js
CHANGED
|
@@ -11,12 +11,12 @@ describe('Asset - External Build', function () {
|
|
|
11
11
|
this.timeout(t.timeout);
|
|
12
12
|
|
|
13
13
|
after(async function () {
|
|
14
|
-
|
|
14
|
+
await fs.remove(publicBuildPath);
|
|
15
15
|
await t.destroy(apos);
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
beforeEach(async function () {
|
|
19
|
-
|
|
19
|
+
await fs.remove(publicBuildPath);
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
it('should register external build module', async function () {
|
|
@@ -260,4 +260,185 @@ describe('Asset - External Build', function () {
|
|
|
260
260
|
'`build` configuration is not identical to the legacy `webpack` configuration'
|
|
261
261
|
);
|
|
262
262
|
});
|
|
263
|
+
|
|
264
|
+
it('should inject custom bundles dynamic CSS (PRO-6904)', async function () {
|
|
265
|
+
await t.destroy(apos);
|
|
266
|
+
|
|
267
|
+
apos = await t.create({
|
|
268
|
+
root: module,
|
|
269
|
+
autoBuild: true,
|
|
270
|
+
modules: {
|
|
271
|
+
'asset-vite': {
|
|
272
|
+
before: '@apostrophecms/asset',
|
|
273
|
+
handlers(self) {
|
|
274
|
+
return {
|
|
275
|
+
'@apostrophecms/asset:afterInit': {
|
|
276
|
+
async registerExternalBuild() {
|
|
277
|
+
self.apos.asset.configureBuildModule(self, {
|
|
278
|
+
alias: 'vite',
|
|
279
|
+
devServer: false,
|
|
280
|
+
hmr: false
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
},
|
|
286
|
+
methods(self) {
|
|
287
|
+
return {
|
|
288
|
+
async build() {
|
|
289
|
+
return {
|
|
290
|
+
entrypoints: []
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
async watch() { },
|
|
294
|
+
async startDevServer() {
|
|
295
|
+
return {
|
|
296
|
+
entrypoints: []
|
|
297
|
+
};
|
|
298
|
+
},
|
|
299
|
+
async entrypoints() {
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Mock the build manifest
|
|
309
|
+
apos.asset.currentBuildManifest = {
|
|
310
|
+
sourceMapsRoot: null,
|
|
311
|
+
devServerUrl: null,
|
|
312
|
+
hmrTypes: [],
|
|
313
|
+
entrypoints: [
|
|
314
|
+
{
|
|
315
|
+
name: 'src',
|
|
316
|
+
type: 'index',
|
|
317
|
+
scenes: [ 'apos', 'public' ],
|
|
318
|
+
outputs: [ 'css', 'js' ],
|
|
319
|
+
condition: 'module',
|
|
320
|
+
manifest: {
|
|
321
|
+
root: 'dist',
|
|
322
|
+
name: 'src',
|
|
323
|
+
devServer: false
|
|
324
|
+
},
|
|
325
|
+
bundles: [
|
|
326
|
+
'apos-src-module-bundle.js',
|
|
327
|
+
'apos-bundle.css',
|
|
328
|
+
'public-src-module-bundle.js',
|
|
329
|
+
'public-bundle.css'
|
|
330
|
+
]
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
name: 'counter-react',
|
|
334
|
+
type: 'custom',
|
|
335
|
+
scenes: [ 'counter-react' ],
|
|
336
|
+
outputs: [ 'css', 'js' ],
|
|
337
|
+
condition: 'module',
|
|
338
|
+
prologue: '',
|
|
339
|
+
ignoreSources: [],
|
|
340
|
+
manifest: {
|
|
341
|
+
root: 'dist',
|
|
342
|
+
name: 'counter-react',
|
|
343
|
+
devServer: false
|
|
344
|
+
},
|
|
345
|
+
bundles: [ 'counter-react-module-bundle.js' ]
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
name: 'counter-svelte',
|
|
349
|
+
type: 'custom',
|
|
350
|
+
scenes: [ 'counter-svelte' ],
|
|
351
|
+
outputs: [ 'css', 'js' ],
|
|
352
|
+
condition: 'module',
|
|
353
|
+
manifest: {
|
|
354
|
+
root: 'dist',
|
|
355
|
+
name: 'counter-svelte',
|
|
356
|
+
devServer: false
|
|
357
|
+
},
|
|
358
|
+
bundles: [
|
|
359
|
+
'counter-svelte-module-bundle.js',
|
|
360
|
+
'counter-svelte-bundle.css'
|
|
361
|
+
]
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
name: 'counter-vue',
|
|
365
|
+
type: 'custom',
|
|
366
|
+
scenes: [ 'counter-vue' ],
|
|
367
|
+
outputs: [ 'css', 'js' ],
|
|
368
|
+
condition: 'module',
|
|
369
|
+
manifest: {
|
|
370
|
+
root: 'dist',
|
|
371
|
+
name: 'counter-vue',
|
|
372
|
+
devServer: false
|
|
373
|
+
},
|
|
374
|
+
bundles: [ 'counter-vue-module-bundle.js', 'counter-vue-bundle.css' ]
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
name: 'apos',
|
|
378
|
+
type: 'apos',
|
|
379
|
+
scenes: [ 'apos' ],
|
|
380
|
+
outputs: [ 'js' ],
|
|
381
|
+
condition: 'module',
|
|
382
|
+
manifest: {
|
|
383
|
+
root: 'dist',
|
|
384
|
+
name: 'apos',
|
|
385
|
+
devServer: false
|
|
386
|
+
},
|
|
387
|
+
bundles: [ 'apos-module-bundle.js', 'apos-bundle.css' ]
|
|
388
|
+
}
|
|
389
|
+
]
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// `counter-svelte` has `ui/src/counter-svelte.scss`.
|
|
393
|
+
// `counter-vue` doesn't have any CSS but has dynamic CSS, extracted
|
|
394
|
+
// from Vue components.
|
|
395
|
+
apos.asset.extraBundles = {
|
|
396
|
+
js: [ 'counter-react', 'counter-svelte', 'counter-vue' ],
|
|
397
|
+
css: [ 'counter-svelte ' ]
|
|
398
|
+
};
|
|
399
|
+
apos.asset.rebundleModules = [];
|
|
400
|
+
|
|
401
|
+
const payload = {
|
|
402
|
+
page: {
|
|
403
|
+
type: '@apostrophecms/home-page'
|
|
404
|
+
},
|
|
405
|
+
scene: 'apos',
|
|
406
|
+
template: '@apostrophecms/home-page:page',
|
|
407
|
+
content: '[stylesheets-placeholder:1]\n[scripts-placeholder:1]',
|
|
408
|
+
scriptsPlaceholder: '[scripts-placeholder:1]',
|
|
409
|
+
stylesheetsPlaceholder: '[stylesheets-placeholder:1]',
|
|
410
|
+
widgetsBundles: { 'counter-vue': {} }
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const injected = apos.template.insertBundlesMarkup(payload);
|
|
414
|
+
|
|
415
|
+
// All bundles are injected, including the dynamic CSS from `counter-vue`
|
|
416
|
+
const actual = injected.replace(/>\s+</g, '><');
|
|
417
|
+
const expected = '<link rel="stylesheet" href="/apos-frontend/default/apos-bundle.css">' +
|
|
418
|
+
'<link rel="stylesheet" href="/apos-frontend/default/counter-svelte-bundle.css">' +
|
|
419
|
+
'<link rel="stylesheet" href="/apos-frontend/default/counter-vue-bundle.css">' +
|
|
420
|
+
'<script type="module" src="/apos-frontend/default/apos-src-module-bundle.js"></script>' +
|
|
421
|
+
'<script type="module" src="/apos-frontend/default/apos-module-bundle.js"></script>' +
|
|
422
|
+
'<script type="module" src="/apos-frontend/default/counter-react-module-bundle.js"></script>' +
|
|
423
|
+
'<script type="module" src="/apos-frontend/default/counter-svelte-module-bundle.js"></script>' +
|
|
424
|
+
'<script type="module" src="/apos-frontend/default/counter-vue-module-bundle.js"></script>';
|
|
425
|
+
|
|
426
|
+
assert.equal(actual, expected, 'Bundles are not injected correctly');
|
|
427
|
+
|
|
428
|
+
const injectedPublic = apos.template.insertBundlesMarkup({
|
|
429
|
+
...payload,
|
|
430
|
+
scene: 'public'
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// Showcases that the only the dynamic Vue CSS is injected, because the widget
|
|
434
|
+
// owning the bundle counter-vue is present.
|
|
435
|
+
const actualPublic = injectedPublic.replace(/>\s+</g, '><');
|
|
436
|
+
const expectedPublic = '<link rel="stylesheet" href="/apos-frontend/default/public-bundle.css">' +
|
|
437
|
+
'<link rel="stylesheet" href="/apos-frontend/default/counter-vue-bundle.css">' +
|
|
438
|
+
'<script type="module" src="/apos-frontend/default/public-src-module-bundle.js"></script>' +
|
|
439
|
+
'<script type="module" src="/apos-frontend/default/counter-vue-module-bundle.js"></script>';
|
|
440
|
+
|
|
441
|
+
assert.equal(actualPublic, expectedPublic, 'Bundles are not injected correctly');
|
|
442
|
+
|
|
443
|
+
});
|
|
263
444
|
});
|