apostrophe 4.29.0 → 4.30.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/.claude/settings.local.json +15 -0
- package/CHANGELOG.md +34 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBar.vue +0 -1
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarMenu.vue +25 -8
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBreakpointPreviewMode.vue +9 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +20 -2
- package/modules/@apostrophecms/area/index.js +10 -5
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +2 -0
- package/modules/@apostrophecms/command-menu/ui/apos/components/TheAposCommandMenu.vue +11 -1
- package/modules/@apostrophecms/i18n/i18n/en.json +8 -0
- package/modules/@apostrophecms/i18n/index.js +1 -8
- package/modules/@apostrophecms/image-widget/index.js +29 -1
- package/modules/@apostrophecms/layout-widget/index.js +124 -2
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue +89 -6
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridLayout.vue +2 -2
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridManager.vue +2 -2
- package/modules/@apostrophecms/layout-widget/ui/apos/layout.css +8 -0
- package/modules/@apostrophecms/layout-widget/ui/src/layout.css +1 -1
- package/modules/@apostrophecms/layout-widget/views/widget.html +3 -3
- package/modules/@apostrophecms/login/index.js +13 -15
- package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +2 -1
- package/modules/@apostrophecms/oembed/index.js +18 -13
- package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedIcon.vue +2 -0
- package/modules/@apostrophecms/rich-text-widget/index.js +36 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputCheckboxes.js +1 -1
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRadio.js +1 -1
- package/modules/@apostrophecms/styles/index.js +16 -0
- package/modules/@apostrophecms/styles/lib/handlers.js +6 -0
- package/modules/@apostrophecms/styles/lib/methods.js +93 -0
- package/modules/@apostrophecms/styles/lib/presets.js +17 -0
- package/modules/@apostrophecms/styles/ui/apos/universal/render.mjs +10 -1
- package/modules/@apostrophecms/submitted-draft/ui/apos/components/AposSubmittedDraftIcon.vue +1 -0
- package/modules/@apostrophecms/template/views/outerLayoutBase.html +1 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +14 -2
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +29 -5
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuDialog.vue +8 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuItem.vue +5 -2
- package/modules/@apostrophecms/ui/ui/apos/components/AposLocalePicker.vue +14 -1
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_utilities.scss +13 -1
- package/modules/@apostrophecms/util/index.js +4 -0
- package/modules/@apostrophecms/widget-type/index.js +6 -0
- package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +32 -0
- package/package.json +5 -5
- package/test/add-missing-schema-fields-project/node_modules/.package-lock.json +2 -2
- package/test/layout-widget-gap.js +530 -0
- package/test/login.js +122 -1
- package/test/rich-text-widget.js +200 -0
- package/test/styles.js +50 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(timeout 180 npx mocha:*)",
|
|
5
|
+
"Bash(timeout 600 npx mocha:*)",
|
|
6
|
+
"Bash(npm ls:*)",
|
|
7
|
+
"Bash(timeout 540 npx mocha:*)",
|
|
8
|
+
"Bash(echo:*)",
|
|
9
|
+
"Bash(timeout 10 node:*)",
|
|
10
|
+
"Bash(timeout 300 npx mocha:*)",
|
|
11
|
+
"Bash(timeout 60 npx mocha:*)",
|
|
12
|
+
"Bash(timeout 120 npx mocha:*)"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.30.0
|
|
4
|
+
|
|
5
|
+
### Adds
|
|
6
|
+
|
|
7
|
+
- Layout widget gap is now controllable through the styles system, both site-wide via a global `layoutGap` preset and per widget via a `gap` styles field. A new `className` option allows additional CSS classes to be added to the widget grid container.
|
|
8
|
+
|
|
9
|
+
### Fixes
|
|
10
|
+
|
|
11
|
+
- Fixed layout widget not regaining full focus when switching back to Edit content mode.
|
|
12
|
+
- Fixed illegal HTML `id` attribute values generated by the admin UI.
|
|
13
|
+
- Fixed orderable table array items dragging the entire floating window.
|
|
14
|
+
- Fixed keyboard shortcuts for widget operations (copy, cut, paste, duplicate, remove) blocking the browser's native clipboard behavior when no widget was focused. Previously, selecting and copying text while logged in was intercepted unconditionally by the admin UI.
|
|
15
|
+
- Removed duplicate `<meta charset>` tag from `outerLayoutBase.html` and standardized charset to `utf-8`.
|
|
16
|
+
- Updated `apostrophe` and `oembetter` to remove oembed services that no longer support public access, eliminating them as a potential future XSS vector. New `minimumAllowlist` and `minimumEndpoints` options on `@apostrophecms/oembed` allow developers to prune the list further.
|
|
17
|
+
|
|
18
|
+
### Security
|
|
19
|
+
|
|
20
|
+
- **Password reset base URL requirement:** The password reset feature now refuses to operate unless `baseUrl` or `APOS_BASE_URL` is set, preventing a vulnerability where ApostropheCMS could be convinced to send emails with links to attacker-controlled sites. Only affects projects with `passwordReset: true` on the login module. Thanks to [SPIDY](https://github.com/Mujahidkhan525) for reporting.
|
|
21
|
+
- **XSS via full name field:** A malicious full name containing HTML was executed in the page title tooltip in the admin bar, posing an XSS risk to other users. All multi-user projects should update promptly. Thanks to [Muhammad Uwais](https://github.com/MuhammadUwais) for reporting.
|
|
22
|
+
- **XSS via image widget link URL:** Users with editing privileges could trigger arbitrary JavaScript via a `javascript:` URL in the image widget's link URL field. A migration is included to strip any such URLs already in the database. Thanks to [Muhammad Uwais](https://github.com/MuhammadUwais) for reporting.
|
|
23
|
+
- **SSRF via rich text HTML import:** The rich text widget's HTML import feature no longer fetches images from arbitrary hosts, which could be used to probe internal networks or exfiltrate internal images. Configure `imageImportAllowedHostnames` on `@apostrophecms/rich-text-widget` to opt in. Thanks to [Yiğit Şengezer](https://github.com/yigitsengezer) and [Sainithin0309](https://github.com/Sainithin0309) for reporting.
|
|
24
|
+
- **the xmp tag could be used to pass forbidden markup through sanitize-html**, even when xmp itself. This was fixed in `sanitize-html` and the dependency was bumped. Thanks to [Vincenzo Turturro](https://github.com/sushi-gif) for reporting the vulnerability.
|
|
25
|
+
- **the `linkHref` field of image widgets was an XSS vulnerability** because it did not use the `url` field type. This means that a user with editing privileges could potentially carry out XSS. In addition, we have updated the `launder` module to sanitize URLs more robustly for the `url` field type, and bumped that dependency. Also, a database migration is included to clean any XSS attacks that could be present in existing links. Thanks to [Muhammad Uwais](https://github.com/MuhammadUwais) for reporting the issue.
|
|
26
|
+
|
|
27
|
+
### Accessibility
|
|
28
|
+
|
|
29
|
+
- Corrected ARIA semantics on the top admin navigation bar.
|
|
30
|
+
- Improved the document context title (admin bar middle group) and the underlying `AposContextMenu` machinery.
|
|
31
|
+
- Improved the locale switcher (`AposLocalePicker`).
|
|
32
|
+
- The Recently Edited Documents tray icon now exposes its action via `aria-label`.
|
|
33
|
+
- Fixed `.apos-sr-only` so screen-reader-only content is correctly exposed to the accessibility tree.
|
|
34
|
+
- Icon-only context-utility buttons in the admin bar tray (e.g. the global settings cog) now expose their action via `aria-label`.
|
|
35
|
+
|
|
36
|
+
|
|
3
37
|
## 4.29.0 (2026-04-15)
|
|
4
38
|
|
|
5
39
|
### Adds
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<ol
|
|
3
|
-
class="apos-admin-bar__items"
|
|
4
|
-
role="menu"
|
|
5
|
-
>
|
|
2
|
+
<ol class="apos-admin-bar__items">
|
|
6
3
|
<li
|
|
7
4
|
v-if="pageTree"
|
|
8
5
|
class="apos-admin-bar__item"
|
|
@@ -12,7 +9,6 @@
|
|
|
12
9
|
label="apostrophe:pages"
|
|
13
10
|
class="apos-admin-bar__btn"
|
|
14
11
|
:modifiers="['no-motion']"
|
|
15
|
-
role="menuitem"
|
|
16
12
|
action-test-label="page-manager-button"
|
|
17
13
|
@click="emitEvent({ action: '@apostrophecms/page:manager' })"
|
|
18
14
|
/>
|
|
@@ -33,7 +29,6 @@
|
|
|
33
29
|
class: 'apos-admin-bar__btn',
|
|
34
30
|
type: 'subtle'
|
|
35
31
|
}"
|
|
36
|
-
role="menuitem"
|
|
37
32
|
@item-clicked="emitEvent"
|
|
38
33
|
/>
|
|
39
34
|
<Component
|
|
@@ -44,7 +39,6 @@
|
|
|
44
39
|
:modifiers="['no-motion']"
|
|
45
40
|
class="apos-admin-bar__btn"
|
|
46
41
|
:action-test-label="`${item.name}-manager-button`"
|
|
47
|
-
role="menuitem"
|
|
48
42
|
@click="emitEvent(item)"
|
|
49
43
|
/>
|
|
50
44
|
</li>
|
|
@@ -62,7 +56,6 @@
|
|
|
62
56
|
type: 'primary',
|
|
63
57
|
modifiers: ['round', 'no-motion']
|
|
64
58
|
}"
|
|
65
|
-
role="menuitem"
|
|
66
59
|
@item-clicked="emitEvent"
|
|
67
60
|
/>
|
|
68
61
|
</li>
|
|
@@ -89,6 +82,7 @@
|
|
|
89
82
|
:label="item.label"
|
|
90
83
|
:action="item.action"
|
|
91
84
|
:state="trayItemState[item.name] ? [ 'active' ] : []"
|
|
85
|
+
:attrs="trayItemAttrs(item)"
|
|
92
86
|
@click="emitEvent(item)"
|
|
93
87
|
/>
|
|
94
88
|
</template>
|
|
@@ -185,6 +179,29 @@ export default {
|
|
|
185
179
|
} else {
|
|
186
180
|
return item.options.tooltip;
|
|
187
181
|
}
|
|
182
|
+
},
|
|
183
|
+
// Tray utility buttons render icon-only, so the visible label
|
|
184
|
+
// (e.g. "Global Content") is sr-only and doesn't describe what the
|
|
185
|
+
// button does. Make them accessible by providing an aria-label based on
|
|
186
|
+
// the tooltip content.
|
|
187
|
+
trayItemAttrs(item) {
|
|
188
|
+
const tooltip = item.options?.tooltip;
|
|
189
|
+
let key = null;
|
|
190
|
+
if (item.options?.toggle) {
|
|
191
|
+
if (this.trayItemState[item.name] && tooltip?.deactivate) {
|
|
192
|
+
key = tooltip.deactivate;
|
|
193
|
+
} else if (tooltip?.activate) {
|
|
194
|
+
key = tooltip.activate;
|
|
195
|
+
}
|
|
196
|
+
} else if (typeof tooltip === 'string') {
|
|
197
|
+
key = tooltip;
|
|
198
|
+
} else if (tooltip && typeof tooltip.content === 'string') {
|
|
199
|
+
key = tooltip.content;
|
|
200
|
+
}
|
|
201
|
+
if (!key) {
|
|
202
|
+
return {};
|
|
203
|
+
}
|
|
204
|
+
return { 'aria-label': this.$t(key) };
|
|
188
205
|
}
|
|
189
206
|
}
|
|
190
207
|
};
|
package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBreakpointPreviewMode.vue
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
:label="screen.label"
|
|
15
15
|
:tooltip="$t(screen.label)"
|
|
16
16
|
:title="$t(screen.label)"
|
|
17
|
+
:attrs="shortcutAttrs(screen)"
|
|
17
18
|
:icon="screen.icon"
|
|
18
19
|
:icon-only="true"
|
|
19
20
|
type="subtle"
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
:active-item="mode"
|
|
37
38
|
:center-on-icon="true"
|
|
38
39
|
menu-placement="bottom-end"
|
|
40
|
+
:dialog-label="'apostrophe:breakpointPreviewSelectMenu'"
|
|
39
41
|
@item-clicked="selectBreakpoint"
|
|
40
42
|
/>
|
|
41
43
|
<Transition>
|
|
@@ -320,6 +322,13 @@ export default {
|
|
|
320
322
|
},
|
|
321
323
|
setShowDropdown() {
|
|
322
324
|
this.showDropdown = Object.values(this.screens).some(({ shortcut }) => !shortcut);
|
|
325
|
+
},
|
|
326
|
+
shortcutAttrs(screen) {
|
|
327
|
+
return {
|
|
328
|
+
'aria-label': this.$t('apostrophe:breakpointPreviewShortcut', {
|
|
329
|
+
breakpoint: this.$t(screen.label)
|
|
330
|
+
})
|
|
331
|
+
};
|
|
323
332
|
}
|
|
324
333
|
}
|
|
325
334
|
};
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
:disabled="hasCustomUi || isUnpublished"
|
|
34
34
|
:center-on-icon="true"
|
|
35
35
|
menu-placement="bottom-end"
|
|
36
|
+
:dialog-label="'apostrophe:publicationStatusMenu'"
|
|
37
|
+
:trigger-aria-label="draftTriggerAriaLabel"
|
|
36
38
|
@item-clicked="switchDraftMode"
|
|
37
39
|
/>
|
|
38
40
|
<AposLabel
|
|
@@ -56,6 +58,15 @@
|
|
|
56
58
|
<script>
|
|
57
59
|
import dayjs from 'dayjs';
|
|
58
60
|
|
|
61
|
+
function escapeHtml(s) {
|
|
62
|
+
return String(s)
|
|
63
|
+
.replace(/&/g, '&')
|
|
64
|
+
.replace(/</g, '<')
|
|
65
|
+
.replace(/>/g, '>')
|
|
66
|
+
.replace(/"/g, '"')
|
|
67
|
+
.replace(/'/g, ''');
|
|
68
|
+
}
|
|
69
|
+
|
|
59
70
|
export default {
|
|
60
71
|
name: 'TheAposContextTitle',
|
|
61
72
|
props: {
|
|
@@ -78,8 +89,8 @@ export default {
|
|
|
78
89
|
if (this.context.updatedBy) {
|
|
79
90
|
const editor = this.context.updatedBy;
|
|
80
91
|
editorLabel = '';
|
|
81
|
-
editorLabel += editor.title ? `${editor.title} ` : '';
|
|
82
|
-
editorLabel += editor.username ? `(${editor.username})` : '';
|
|
92
|
+
editorLabel += editor.title ? `${escapeHtml(editor.title)} ` : '';
|
|
93
|
+
editorLabel += editor.username ? `(${escapeHtml(editor.username)})` : '';
|
|
83
94
|
}
|
|
84
95
|
return editorLabel;
|
|
85
96
|
},
|
|
@@ -91,6 +102,13 @@ export default {
|
|
|
91
102
|
type: 'quiet'
|
|
92
103
|
};
|
|
93
104
|
},
|
|
105
|
+
draftTriggerAriaLabel() {
|
|
106
|
+
return this.$t('apostrophe:publicationStatusTrigger', {
|
|
107
|
+
status: this.$t(
|
|
108
|
+
this.draftMode === 'draft' ? 'apostrophe:draft' : 'apostrophe:published'
|
|
109
|
+
)
|
|
110
|
+
});
|
|
111
|
+
},
|
|
94
112
|
isUnpublished() {
|
|
95
113
|
return !this.context.lastPublishedAt;
|
|
96
114
|
},
|
|
@@ -19,7 +19,8 @@ module.exports = {
|
|
|
19
19
|
action: {
|
|
20
20
|
type: 'command-menu-area-cut-widget'
|
|
21
21
|
},
|
|
22
|
-
shortcut: 'Ctrl+X Meta+X'
|
|
22
|
+
shortcut: 'Ctrl+X Meta+X',
|
|
23
|
+
requireWidgetFocus: true
|
|
23
24
|
},
|
|
24
25
|
[`${self.__meta.name}:copy-widget`]: {
|
|
25
26
|
type: 'item',
|
|
@@ -27,7 +28,8 @@ module.exports = {
|
|
|
27
28
|
action: {
|
|
28
29
|
type: 'command-menu-area-copy-widget'
|
|
29
30
|
},
|
|
30
|
-
shortcut: 'Ctrl+C Meta+C'
|
|
31
|
+
shortcut: 'Ctrl+C Meta+C',
|
|
32
|
+
requireWidgetFocus: true
|
|
31
33
|
},
|
|
32
34
|
[`${self.__meta.name}:paste-widget`]: {
|
|
33
35
|
type: 'item',
|
|
@@ -35,7 +37,8 @@ module.exports = {
|
|
|
35
37
|
action: {
|
|
36
38
|
type: 'command-menu-area-paste-widget'
|
|
37
39
|
},
|
|
38
|
-
shortcut: 'Ctrl+V Meta+V'
|
|
40
|
+
shortcut: 'Ctrl+V Meta+V',
|
|
41
|
+
requireWidgetFocus: true
|
|
39
42
|
},
|
|
40
43
|
[`${self.__meta.name}:duplicate-widget`]: {
|
|
41
44
|
type: 'item',
|
|
@@ -43,7 +46,8 @@ module.exports = {
|
|
|
43
46
|
action: {
|
|
44
47
|
type: 'command-menu-area-duplicate-widget'
|
|
45
48
|
},
|
|
46
|
-
shortcut: 'Ctrl+Shift+D Meta+Shift+D'
|
|
49
|
+
shortcut: 'Ctrl+Shift+D Meta+Shift+D',
|
|
50
|
+
requireWidgetFocus: true
|
|
47
51
|
},
|
|
48
52
|
[`${self.__meta.name}:remove-widget`]: {
|
|
49
53
|
type: 'item',
|
|
@@ -51,7 +55,8 @@ module.exports = {
|
|
|
51
55
|
action: {
|
|
52
56
|
type: 'command-menu-area-remove-widget'
|
|
53
57
|
},
|
|
54
|
-
shortcut: 'Backspace'
|
|
58
|
+
shortcut: 'Backspace',
|
|
59
|
+
requireWidgetFocus: true
|
|
55
60
|
}
|
|
56
61
|
},
|
|
57
62
|
modal: {
|
|
@@ -536,6 +536,7 @@ export default {
|
|
|
536
536
|
apos.bus.$on('widget-focus-parent', this.focusParent);
|
|
537
537
|
apos.bus.$on('context-menu-toggled', this.getFocusForMenu);
|
|
538
538
|
apos.bus.$on('suppress-focused-widget-controls', this.doSuppressWidgetControls);
|
|
539
|
+
apos.bus.$on('clear-focused-widget-control-suppression', this.clearSuppressionFlags);
|
|
539
540
|
|
|
540
541
|
this.breadcrumbs.$lastEl = this.$el;
|
|
541
542
|
|
|
@@ -573,6 +574,7 @@ export default {
|
|
|
573
574
|
// Remove the focus parent listener when unmounted
|
|
574
575
|
apos.bus.$off('widget-focus-parent', this.focusParent);
|
|
575
576
|
apos.bus.$off('suppress-focused-widget-controls', this.doSuppressWidgetControls);
|
|
577
|
+
apos.bus.$off('clear-focused-widget-control-suppression', this.clearSuppressionFlags);
|
|
576
578
|
window.removeEventListener('scroll', this.stickyControlsScroll);
|
|
577
579
|
window.removeEventListener('resize', this.stickyControlsResize);
|
|
578
580
|
this.unregisterFromGraph();
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { mapActions, mapState } from 'pinia';
|
|
11
11
|
import AposThemeMixin from 'Modules/@apostrophecms/ui/mixins/AposThemeMixin';
|
|
12
12
|
import { useModalStore } from 'Modules/@apostrophecms/ui/stores/modal';
|
|
13
|
+
import { useWidgetStore } from 'Modules/@apostrophecms/ui/stores/widget';
|
|
13
14
|
|
|
14
15
|
export default {
|
|
15
16
|
name: 'TheAposCommandMenu',
|
|
@@ -50,7 +51,13 @@ export default {
|
|
|
50
51
|
.flatMap(command => {
|
|
51
52
|
return command.shortcut
|
|
52
53
|
.split(' ')
|
|
53
|
-
.map(shortcut => [
|
|
54
|
+
.map(shortcut => [
|
|
55
|
+
shortcut.toUpperCase(),
|
|
56
|
+
{
|
|
57
|
+
...command.action,
|
|
58
|
+
requireWidgetFocus: command.requireWidgetFocus || false
|
|
59
|
+
}
|
|
60
|
+
]);
|
|
54
61
|
});
|
|
55
62
|
})
|
|
56
63
|
);
|
|
@@ -111,6 +118,9 @@ export default {
|
|
|
111
118
|
? keys.slice('SHIFT+'.length)
|
|
112
119
|
: keys];
|
|
113
120
|
if (action) {
|
|
121
|
+
if (action.requireWidgetFocus && !useWidgetStore().focusedWidget) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
114
124
|
event.preventDefault();
|
|
115
125
|
apos.bus.$emit(action.type, action.payload);
|
|
116
126
|
return;
|
|
@@ -89,6 +89,8 @@
|
|
|
89
89
|
"breakpointPreviewExit": "Exit",
|
|
90
90
|
"breakpointPreviewMobile": "Mobile",
|
|
91
91
|
"breakpointPreviewSelect": "Select Breakpoint",
|
|
92
|
+
"breakpointPreviewSelectMenu": "Breakpoint preview menu",
|
|
93
|
+
"breakpointPreviewShortcut": "Preview at {{ breakpoint }} breakpoint",
|
|
92
94
|
"breakpointPreviewTablet": "Tablet",
|
|
93
95
|
"browse": "Browse",
|
|
94
96
|
"browseDocType": "Browse {{ type }}",
|
|
@@ -387,6 +389,7 @@
|
|
|
387
389
|
"mediaUploadViaDrop": "Drop ’em when you’re ready",
|
|
388
390
|
"mediaUploadViaExplorer": "Or click to open the file explorer",
|
|
389
391
|
"mergeCells": "Merge Cells",
|
|
392
|
+
"menu": "Menu",
|
|
390
393
|
"minLabel": "Min:",
|
|
391
394
|
"minSize": "Min size of {{ width }}x{{ height }}",
|
|
392
395
|
"minimumSize": "Minimum size of {{ width }} x {{ height }} px",
|
|
@@ -475,6 +478,8 @@
|
|
|
475
478
|
"publishBeforeUsingTooltip": "Publish this content before using it in a relationship",
|
|
476
479
|
"publishType": "Publish {{ type }}",
|
|
477
480
|
"published": "Published",
|
|
481
|
+
"publicationStatusMenu": "Publication status",
|
|
482
|
+
"publicationStatusTrigger": "Publication status: {{ status }}. Change publication status.",
|
|
478
483
|
"publishingBatchConfirmation": "Are you sure you want to publish {{ count }} {{ type }}?",
|
|
479
484
|
"publishingBatchConfirmationButton": "Yes, publish content.",
|
|
480
485
|
"rawCssAndJs": "Raw CSS and JS",
|
|
@@ -490,6 +495,7 @@
|
|
|
490
495
|
"recentlyEditedActionSubmitted": "Submitted",
|
|
491
496
|
"recentlyEditedCurrentUser": "Me ({{ user }})",
|
|
492
497
|
"recentlyEditedDocuments": "Recently edited documents",
|
|
498
|
+
"recentlyEditedManagerOpen": "Open recently edited documents manager",
|
|
493
499
|
"recentlyEditedEditedBy": "Edited by",
|
|
494
500
|
"recentlyEditedClearAllFilters": "Clear all filters",
|
|
495
501
|
"recentlyEditedClearSearch": "Clear search",
|
|
@@ -643,6 +649,8 @@
|
|
|
643
649
|
"styleGradientAngle": "Angle",
|
|
644
650
|
"styleGradientEnd": "End Color",
|
|
645
651
|
"styleGradientStart": "Start Color",
|
|
652
|
+
"styleLayoutGap": "Layout Gap",
|
|
653
|
+
"styleLayoutGapHelp": "Sets the spacing between columns inside layout sections across the site.",
|
|
646
654
|
"styleLeft": "Left",
|
|
647
655
|
"styleMargin": "Margin",
|
|
648
656
|
"styleOverlayColor": "Overlay Color",
|
|
@@ -29,10 +29,6 @@
|
|
|
29
29
|
// in the same language as the website content.
|
|
30
30
|
// Example: `defaultAdminLocale: 'fr'`.
|
|
31
31
|
//
|
|
32
|
-
// ### `encoding`
|
|
33
|
-
//
|
|
34
|
-
// Defaults to `'utf-8'`. You almost certainly do not want to change this.
|
|
35
|
-
//
|
|
36
32
|
// ### `slugDirection`
|
|
37
33
|
//
|
|
38
34
|
// Controls the default `direction` value of slug schema. Can be `ltr`, `rtl` or
|
|
@@ -81,8 +77,6 @@ module.exports = {
|
|
|
81
77
|
},
|
|
82
78
|
// If true, slugifying will strip accents from Latin characters
|
|
83
79
|
stripUrlAccents: false,
|
|
84
|
-
// You almost certainly do not want to change this
|
|
85
|
-
encoding: 'utf-8',
|
|
86
80
|
slugDirection: 'ltr'
|
|
87
81
|
},
|
|
88
82
|
async init(self) {
|
|
@@ -166,7 +160,6 @@ module.exports = {
|
|
|
166
160
|
await self.i18next.init(i18nextOptions);
|
|
167
161
|
self.addInitialResources();
|
|
168
162
|
self.enableBrowserData();
|
|
169
|
-
self.encoding = self.options.encoding;
|
|
170
163
|
},
|
|
171
164
|
handlers(self) {
|
|
172
165
|
return {
|
|
@@ -1369,7 +1362,7 @@ module.exports = {
|
|
|
1369
1362
|
helpers(self) {
|
|
1370
1363
|
return {
|
|
1371
1364
|
encoding() {
|
|
1372
|
-
return
|
|
1365
|
+
return 'utf-8';
|
|
1373
1366
|
}
|
|
1374
1367
|
};
|
|
1375
1368
|
}
|
|
@@ -102,7 +102,7 @@ module.exports = {
|
|
|
102
102
|
linkHref: {
|
|
103
103
|
label: 'apostrophe:url',
|
|
104
104
|
help: 'apostrophe:linkHrefHelp',
|
|
105
|
-
type: '
|
|
105
|
+
type: 'url',
|
|
106
106
|
required: true,
|
|
107
107
|
if: {
|
|
108
108
|
linkTo: '_url'
|
|
@@ -147,6 +147,7 @@ module.exports = {
|
|
|
147
147
|
self.showPlaceholder = self.options.placeholder !== false;
|
|
148
148
|
self.options.placeholder = true;
|
|
149
149
|
self.determineBestAssetUrl('placeholder');
|
|
150
|
+
self.addCleanLinkHrefMigration();
|
|
150
151
|
},
|
|
151
152
|
handlers(self) {
|
|
152
153
|
return {
|
|
@@ -159,6 +160,33 @@ module.exports = {
|
|
|
159
160
|
},
|
|
160
161
|
methods(self) {
|
|
161
162
|
return {
|
|
163
|
+
// Clear link hrefs that use dangerous URL schemes (e.g.
|
|
164
|
+
// `javascript:`) and may have been stored before the linkHref
|
|
165
|
+
// schema field was changed to `type: 'url'`. Registered per
|
|
166
|
+
// module instance so subclasses of this widget are migrated
|
|
167
|
+
// under their own widget type and migration name.
|
|
168
|
+
addCleanLinkHrefMigration() {
|
|
169
|
+
self.apos.migration.add(
|
|
170
|
+
`${self.__meta.name}:clean-naughty-link-href`,
|
|
171
|
+
async () => {
|
|
172
|
+
await self.apos.migration.eachWidget({}, async (doc, widget, dotPath) => {
|
|
173
|
+
if (widget.type !== self.name) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (!widget.linkHref) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (!self.apos.launder.naughtyHref(widget.linkHref)) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
await self.apos.doc.db.updateOne(
|
|
183
|
+
{ _id: doc._id },
|
|
184
|
+
{ $set: { [`${dotPath}.linkHref`]: '' } }
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
);
|
|
189
|
+
},
|
|
162
190
|
validateAndAddSchemaLabels() {
|
|
163
191
|
const linkWithType = self.options.linkWithType;
|
|
164
192
|
|
|
@@ -19,6 +19,10 @@ module.exports = {
|
|
|
19
19
|
gap: '1.5rem',
|
|
20
20
|
defaultCellHorizontalAlignment: null,
|
|
21
21
|
defaultCellVerticalAlignment: null,
|
|
22
|
+
// Extra class name(s) to append to the rendered layout-widget area
|
|
23
|
+
// wrapper, in addition to the built-in `layout-widget` class. Accepts
|
|
24
|
+
// a string of space-separated class names.
|
|
25
|
+
className: '',
|
|
22
26
|
injectStyles: true,
|
|
23
27
|
minifyStyles: true
|
|
24
28
|
},
|
|
@@ -98,6 +102,25 @@ module.exports = {
|
|
|
98
102
|
validateAndIdentifyTypes() {
|
|
99
103
|
const { column } = self.validateAndIdentifyTypes();
|
|
100
104
|
self.columnWidgetName = column;
|
|
105
|
+
},
|
|
106
|
+
// Detect the widget-style "gap" field (any styles field whose
|
|
107
|
+
// CSS `property` resolves to `gap`). Only the first match is used.
|
|
108
|
+
// Also detect whether the @apostrophecms/styles module has a
|
|
109
|
+
// site-wide layout gap field configured (via the `layoutGap`
|
|
110
|
+
// preset / `layoutGapDefault: true` marker).
|
|
111
|
+
detectGapFields() {
|
|
112
|
+
const widgetGapFields = self.apos.styles
|
|
113
|
+
.fieldsWithProperty(self.schema, 'gap');
|
|
114
|
+
if (widgetGapFields.length > 1) {
|
|
115
|
+
self.apos.util.warn(
|
|
116
|
+
`[${self.__meta.name}] Multiple style fields produce the ` +
|
|
117
|
+
`CSS \`gap\` property (${widgetGapFields.join(', ')}). ` +
|
|
118
|
+
'Only the first one will be honoured as the widget-scope gap.'
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
self.widgetGapFieldName = widgetGapFields[0] || null;
|
|
122
|
+
self.globalGapEnabled = !!self.apos.modules['@apostrophecms/styles']
|
|
123
|
+
?.layoutGapFieldName;
|
|
101
124
|
}
|
|
102
125
|
}
|
|
103
126
|
};
|
|
@@ -118,6 +141,17 @@ module.exports = {
|
|
|
118
141
|
defaultCellHorizontalAlignment: self.options.defaultCellHorizontalAlignment,
|
|
119
142
|
defaultCellVerticalAlignment: self.options.defaultCellVerticalAlignment
|
|
120
143
|
},
|
|
144
|
+
widgetGapFieldName: self.widgetGapFieldName || null,
|
|
145
|
+
widgetGapFieldUnit: self.widgetGapFieldName
|
|
146
|
+
? (self.apos.styles
|
|
147
|
+
.getFieldByPath(self.schema, self.widgetGapFieldName)
|
|
148
|
+
?.unit || '')
|
|
149
|
+
: '',
|
|
150
|
+
globalGapEnabled: !!self.globalGapEnabled,
|
|
151
|
+
// Opt-in flag read by the generic widget editor: when true,
|
|
152
|
+
// it broadcasts `apos-widget-live-preview` events on the apos bus
|
|
153
|
+
// during the style-only fast path.
|
|
154
|
+
subscribesToLivePreview: !!self.widgetGapFieldName,
|
|
121
155
|
columnWidgetName: self.columnWidgetName
|
|
122
156
|
};
|
|
123
157
|
},
|
|
@@ -132,6 +166,7 @@ module.exports = {
|
|
|
132
166
|
defaultCellHorizontalAlignment,
|
|
133
167
|
defaultCellVerticalAlignment
|
|
134
168
|
} = self.options;
|
|
169
|
+
const widgetGap = self.resolveWidgetGap(widget);
|
|
135
170
|
return {
|
|
136
171
|
..._super(widget, { scene }),
|
|
137
172
|
columns,
|
|
@@ -141,13 +176,53 @@ module.exports = {
|
|
|
141
176
|
tablet,
|
|
142
177
|
gap,
|
|
143
178
|
defaultCellHorizontalAlignment,
|
|
144
|
-
defaultCellVerticalAlignment
|
|
179
|
+
defaultCellVerticalAlignment,
|
|
180
|
+
_gap: widgetGap,
|
|
181
|
+
_gapHasGlobal: !!self.globalGapEnabled
|
|
145
182
|
};
|
|
146
183
|
}
|
|
147
184
|
};
|
|
148
185
|
},
|
|
149
186
|
methods(self) {
|
|
150
187
|
return {
|
|
188
|
+
// Resolve the widget-scope gap for a widget instance, if this
|
|
189
|
+
// module declares a styles field with `property: 'gap'`. Returns
|
|
190
|
+
// the resolved value (with unit, when applicable). When the
|
|
191
|
+
// widget has no explicit value, falls back to the field's `def`.
|
|
192
|
+
// Returns `null` only when no widget gap field is
|
|
193
|
+
// configured, no widget value is set, and no `def` is declared
|
|
194
|
+
// on the field.
|
|
195
|
+
resolveWidgetGap(widget) {
|
|
196
|
+
if (!self.widgetGapFieldName || !widget) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const field = self.apos.styles.getFieldByPath(
|
|
200
|
+
self.schema, self.widgetGapFieldName
|
|
201
|
+
);
|
|
202
|
+
let value = self.apos.util.get(widget, self.widgetGapFieldName);
|
|
203
|
+
if (value === null || value === undefined || value === '') {
|
|
204
|
+
if (field?.def === null || field?.def === undefined || field?.def === '') {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
value = field.def;
|
|
208
|
+
}
|
|
209
|
+
if (field?.unit && typeof value !== 'string') {
|
|
210
|
+
return `${value}${field.unit}`;
|
|
211
|
+
}
|
|
212
|
+
return value;
|
|
213
|
+
},
|
|
214
|
+
// Determine whether the inline `--grid-gap` CSS variable should
|
|
215
|
+
// be omitted on the grid container so the global cascade
|
|
216
|
+
// (`var(--apos-layout-gap, …)`) can take effect. The inline var
|
|
217
|
+
// is omitted whenever:
|
|
218
|
+
// - no widget-scope gap value is set, AND
|
|
219
|
+
// - the global layout-gap field is configured.
|
|
220
|
+
shouldOmitInlineGap(widget, global) {
|
|
221
|
+
if (!self.globalGapEnabled) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
return self.resolveWidgetGap(widget) === null;
|
|
225
|
+
},
|
|
151
226
|
publicCssNodes(req) {
|
|
152
227
|
return [
|
|
153
228
|
{
|
|
@@ -277,11 +352,13 @@ module.exports = {
|
|
|
277
352
|
const mobileBreakpointPlus = mobileBreakpoint + 1;
|
|
278
353
|
const tabletBreakpoint = self.options.tablet?.breakpoint || 1024;
|
|
279
354
|
const tabletBreakpointPlus = tabletBreakpoint + 1;
|
|
355
|
+
const gapDefault = self.options.gap || '0';
|
|
280
356
|
cssContent = cssContent
|
|
281
357
|
.replace(/\{\$mobile\}/g, mobileBreakpoint)
|
|
282
358
|
.replace(/\{\$mobile-plus\}/g, mobileBreakpointPlus)
|
|
283
359
|
.replace(/\{\$tablet\}/g, tabletBreakpoint)
|
|
284
|
-
.replace(/\{\$tablet-plus\}/g, tabletBreakpointPlus)
|
|
360
|
+
.replace(/\{\$tablet-plus\}/g, tabletBreakpointPlus)
|
|
361
|
+
.replace(/\{\$gap-default\}/g, gapDefault);
|
|
285
362
|
|
|
286
363
|
return self.processCss(cssContent, scene);
|
|
287
364
|
},
|
|
@@ -371,6 +448,51 @@ module.exports = {
|
|
|
371
448
|
(a.order ?? 0) - (b.order ?? 0)
|
|
372
449
|
);
|
|
373
450
|
return items[items.length - 1]._id;
|
|
451
|
+
},
|
|
452
|
+
// Compute the `--grid-gap: <value>;` declaration to inline on the
|
|
453
|
+
// grid container, or an empty string when the cascade should
|
|
454
|
+
// resolve gap via `var(--apos-layout-gap, …)` instead.
|
|
455
|
+
// Honours the priority order:
|
|
456
|
+
// 1. Widget-style gap (when set on this widget instance).
|
|
457
|
+
// 2. Static module option (BC) — when no global gap field is
|
|
458
|
+
// configured, or it has no value.
|
|
459
|
+
// 3. Otherwise, omit the inline var so the global cascade wins.
|
|
460
|
+
// Must be invoked via the widget's own module namespace —
|
|
461
|
+
// `apos.modules[data.manager.__meta.name].gapInlineCss(...)` —
|
|
462
|
+
// so that `self` resolves to the actual subclass and picks up
|
|
463
|
+
// its `widgetGapFieldName` / `globalGapEnabled`.
|
|
464
|
+
gapInlineCss(widget, options, global) {
|
|
465
|
+
const widgetGap = self.resolveWidgetGap(widget);
|
|
466
|
+
if (widgetGap !== null) {
|
|
467
|
+
return ` --grid-gap: ${widgetGap};`;
|
|
468
|
+
}
|
|
469
|
+
if (self.shouldOmitInlineGap(widget, global)) {
|
|
470
|
+
return '';
|
|
471
|
+
}
|
|
472
|
+
const fallback = (options && options.gap) || self.options.gap || '0';
|
|
473
|
+
return ` --grid-gap: ${fallback};`;
|
|
474
|
+
},
|
|
475
|
+
// Build the `aposParentOptions` payload passed by the rendered
|
|
476
|
+
// layout-widget area to the in-place editor (AposAreaLayoutEditor).
|
|
477
|
+
// Includes the widget's resolved gap (from its `gap` styles field,
|
|
478
|
+
// when present) so the live editor's grid container reflects the
|
|
479
|
+
// saved per-widget value rather than only the static module
|
|
480
|
+
// option. Sets `gap: null` to signal the editor to omit
|
|
481
|
+
// `--grid-gap` so the global cascade resolves it through
|
|
482
|
+
// `:root { --apos-layout-gap }`. Must be invoked via the widget's
|
|
483
|
+
// own module namespace, like `gapInlineCss`.
|
|
484
|
+
parentOptionsForArea(widget, options, global) {
|
|
485
|
+
const opts = {
|
|
486
|
+
...(options || {}),
|
|
487
|
+
widgetId: widget._id
|
|
488
|
+
};
|
|
489
|
+
const widgetGap = self.resolveWidgetGap(widget);
|
|
490
|
+
if (widgetGap !== null) {
|
|
491
|
+
opts.gap = widgetGap;
|
|
492
|
+
} else if (self.shouldOmitInlineGap(widget, global)) {
|
|
493
|
+
opts.gap = null;
|
|
494
|
+
}
|
|
495
|
+
return opts;
|
|
374
496
|
}
|
|
375
497
|
};
|
|
376
498
|
}
|