apostrophe 4.28.1 → 4.29.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 +29 -4
- package/README.md +2 -2
- package/defaults.js +1 -0
- package/lib/safe-json-script.js +27 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarLocale.vue +1 -1
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +1 -0
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +3 -5
- package/modules/@apostrophecms/area/ui/apos/components/AposBreadcrumbOperations.vue +13 -1
- package/modules/@apostrophecms/asset/lib/globalIcons.js +3 -0
- package/modules/@apostrophecms/attachment/index.js +43 -1
- package/modules/@apostrophecms/color-field/index.js +7 -1
- package/modules/@apostrophecms/doc/index.js +11 -1
- package/modules/@apostrophecms/doc-type/index.js +165 -32
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +1 -1
- package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +104 -59
- package/modules/@apostrophecms/file/index.js +109 -8
- package/modules/@apostrophecms/i18n/i18n/de.json +0 -2
- package/modules/@apostrophecms/i18n/i18n/en.json +40 -1
- package/modules/@apostrophecms/i18n/i18n/es.json +0 -1
- package/modules/@apostrophecms/i18n/i18n/fr.json +0 -1
- package/modules/@apostrophecms/i18n/i18n/it.json +0 -1
- package/modules/@apostrophecms/i18n/i18n/pt-BR.json +0 -1
- package/modules/@apostrophecms/i18n/i18n/sk.json +0 -1
- package/modules/@apostrophecms/i18n/ui/apos/apps/AposI18nBatchReporting.js +18 -1
- package/modules/@apostrophecms/i18n/ui/apos/apps/AposI18nLocalizeActions.js +50 -0
- package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +56 -13
- package/modules/@apostrophecms/image/ui/apos/components/AposImageRelationshipEditor.vue +8 -2
- package/modules/@apostrophecms/layout-column-widget/index.js +156 -163
- package/modules/@apostrophecms/layout-widget/index.js +7 -2
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue +6 -11
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridColumn.vue +3 -5
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridLayout.vue +4 -4
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridManager.vue +0 -16
- package/modules/@apostrophecms/layout-widget/ui/apos/lib/grid-state.mjs +7 -27
- package/modules/@apostrophecms/layout-widget/views/column.html +7 -9
- package/modules/@apostrophecms/login/index.js +39 -40
- package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +17 -2
- package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +3 -2
- package/modules/@apostrophecms/notification/ui/apos/components/AposNotification.vue +1 -0
- package/modules/@apostrophecms/page/index.js +2 -0
- package/modules/@apostrophecms/piece-type/index.js +3 -1
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +1 -0
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerDisplay.vue +5 -0
- package/modules/@apostrophecms/recently-edited/index.js +831 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/components/AposCellTitle.vue +54 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedCombo.vue +454 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedFilterTag.vue +75 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedFilters.vue +287 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedIcon.vue +16 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedManager.vue +346 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/composables/useRecentlyEditedBatch.js +193 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/composables/useRecentlyEditedData.js +276 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/composables/useRecentlyEditedFetch.js +199 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/composables/useRecentlyEditedFilters.js +100 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRelationship.js +8 -4
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputWrapper.js +1 -1
- package/modules/@apostrophecms/styles/index.js +10 -0
- package/modules/@apostrophecms/styles/lib/apiRoutes.js +6 -0
- package/modules/@apostrophecms/styles/lib/handlers.js +5 -0
- package/modules/@apostrophecms/styles/lib/methods.js +9 -3
- package/modules/@apostrophecms/styles/lib/presets.js +119 -0
- package/modules/@apostrophecms/styles/ui/apos/components/TheAposStyles.vue +3 -8
- package/modules/@apostrophecms/styles/ui/apos/composables/AposStyles.js +1 -3
- package/modules/@apostrophecms/styles/ui/apos/render-factory.js +29 -0
- package/modules/@apostrophecms/styles/ui/apos/universal/backgroundHelpers.mjs +140 -0
- package/modules/@apostrophecms/styles/ui/apos/universal/customRules.mjs +105 -0
- package/modules/@apostrophecms/styles/ui/apos/universal/render.mjs +195 -15
- package/modules/@apostrophecms/template/index.js +22 -6
- package/modules/@apostrophecms/ui/ui/apos/components/AposCellContextMenu.vue +2 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +18 -4
- package/modules/@apostrophecms/ui/ui/apos/composables/useInfiniteScroll.js +91 -0
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +1 -0
- package/modules/@apostrophecms/ui/ui/apos/stores/modal.js +5 -2
- package/modules/@apostrophecms/ui/ui/apos/utils/index.js +9 -0
- package/modules/@apostrophecms/url/index.js +38 -4
- package/modules/@apostrophecms/widget-type/index.js +22 -6
- package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +8 -4
- package/package.json +17 -17
- package/test/add-missing-schema-fields-project/node_modules/.package-lock.json +2 -2
- package/test/layout-widget-migration.js +719 -0
- package/test/login-requirements.js +1 -1
- package/test/pieces-public-api.js +80 -0
- package/test/pieces.js +25 -0
- package/test/recently-edited.js +2311 -0
- package/test/schemas.js +39 -3
- package/test/static-build.js +642 -0
- package/test/styles.js +2569 -0
- package/.claude/settings.local.json +0 -15
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposLayoutColControlDialog.vue +0 -171
package/modules/@apostrophecms/recently-edited/ui/apos/composables/useRecentlyEditedFilters.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { computed, ref } from 'vue';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Manages filter state for the recently-edited document manager.
|
|
5
|
+
*
|
|
6
|
+
* @param {import('vue').ComputedRef<Array>} managerFilters - computed ref of
|
|
7
|
+
* filter definitions
|
|
8
|
+
* @param {Object} [initialFilters={}] - Optional initial filter values.
|
|
9
|
+
* When provided (non-empty), fully replaces the default filter state
|
|
10
|
+
* (filter `def` values are NOT used). Values should be arrays and are
|
|
11
|
+
* coerced to scalar/array based on the filter's inputType.
|
|
12
|
+
*/
|
|
13
|
+
export function useRecentlyEditedFilters(managerFilters, initialFilters = {}) {
|
|
14
|
+
const hasInitial = Object.keys(initialFilters).length > 0;
|
|
15
|
+
const filterState = ref(
|
|
16
|
+
Object.fromEntries(
|
|
17
|
+
(managerFilters.value || []).map(f => {
|
|
18
|
+
if (!hasInitial) {
|
|
19
|
+
return [ f.name, filterDefault(f) ];
|
|
20
|
+
}
|
|
21
|
+
return [ f.name, coerceInitialValue(f, initialFilters[f.name]) ];
|
|
22
|
+
})
|
|
23
|
+
)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const hasActiveFilters = computed(() => {
|
|
27
|
+
return Object.values(filterState.value)
|
|
28
|
+
.some(value => {
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
return value.length > 0;
|
|
31
|
+
}
|
|
32
|
+
return value !== null && value !== undefined && value !== '';
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function updateFilter(name, value) {
|
|
37
|
+
filterState.value = {
|
|
38
|
+
...filterState.value,
|
|
39
|
+
[name]: value
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function updateFilters(nextState) {
|
|
44
|
+
filterState.value = nextState;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function clearFilter(name, value) {
|
|
48
|
+
const filter = managerFilters.value.find(f => f.name === name);
|
|
49
|
+
const current = filterState.value[name];
|
|
50
|
+
// For array-valued filters, remove just the specified value.
|
|
51
|
+
if (Array.isArray(current) && value !== undefined) {
|
|
52
|
+
filterState.value = {
|
|
53
|
+
...filterState.value,
|
|
54
|
+
[name]: current.filter(v => v !== value)
|
|
55
|
+
};
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
filterState.value = {
|
|
59
|
+
...filterState.value,
|
|
60
|
+
[name]: filterDefault(filter || {})
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function clearAllFilters() {
|
|
65
|
+
filterState.value = Object.fromEntries(
|
|
66
|
+
managerFilters.value.map(f => [ f.name, filterDefault(f) ])
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
filterState,
|
|
72
|
+
hasActiveFilters,
|
|
73
|
+
updateFilter,
|
|
74
|
+
updateFilters,
|
|
75
|
+
clearFilter,
|
|
76
|
+
clearAllFilters
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function filterDefault(filter) {
|
|
81
|
+
if (filter.def !== undefined) {
|
|
82
|
+
return filter.def;
|
|
83
|
+
}
|
|
84
|
+
return (filter.inputType || 'select') === 'checkbox' ? [] : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Coerce an initial filter value based on the filter's inputType.
|
|
88
|
+
// When initialFilters is active, absent keys get empty defaults (not `def`).
|
|
89
|
+
function coerceInitialValue(filter, value) {
|
|
90
|
+
const isCheckbox = (filter.inputType || 'select') === 'checkbox';
|
|
91
|
+
if (value === undefined || value === null) {
|
|
92
|
+
return isCheckbox ? [] : null;
|
|
93
|
+
}
|
|
94
|
+
const arr = Array.isArray(value) ? value : [ value ];
|
|
95
|
+
if (isCheckbox) {
|
|
96
|
+
return arr;
|
|
97
|
+
}
|
|
98
|
+
// Scalar filter: use value only if exactly one item
|
|
99
|
+
return arr.length === 1 ? arr[0] : null;
|
|
100
|
+
}
|
|
@@ -51,10 +51,14 @@ export default {
|
|
|
51
51
|
},
|
|
52
52
|
// TODO get 'Search' server for better i18n
|
|
53
53
|
placeholder() {
|
|
54
|
-
return this.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
return this.modifiers.some(m => [ 'small', 'micro' ].includes(m))
|
|
55
|
+
? {
|
|
56
|
+
key: 'apostrophe:search'
|
|
57
|
+
}
|
|
58
|
+
: {
|
|
59
|
+
key: 'apostrophe:searchDocType',
|
|
60
|
+
type: this.$t(this.pluralLabel)
|
|
61
|
+
};
|
|
58
62
|
},
|
|
59
63
|
// TODO get 'Browse' for better i18n
|
|
60
64
|
browseLabel() {
|
|
@@ -87,6 +87,16 @@ module.exports = {
|
|
|
87
87
|
self.prependNodes('body', 'stylesheet');
|
|
88
88
|
self.prependNodes('body', 'ui');
|
|
89
89
|
|
|
90
|
+
// Detect if any top-level style field uses a background preset
|
|
91
|
+
// (which includes image relationships requiring attachment annotation).
|
|
92
|
+
// A hack until we analyze the relationship/attachment racing
|
|
93
|
+
// problem more thoroughly and implement a more robust solution.
|
|
94
|
+
// This signals the API render route and post save hook to explicitly
|
|
95
|
+
// re-annotate attachments.
|
|
96
|
+
self.hasBackgroundFields = self.schema.some(
|
|
97
|
+
field => field.customType === 'background'
|
|
98
|
+
);
|
|
99
|
+
|
|
90
100
|
// Removes some automatically added top level groups and
|
|
91
101
|
// provides a default group if none are provided.
|
|
92
102
|
const nonFields = [ 'archived' ];
|
|
@@ -40,6 +40,12 @@ module.exports = self => {
|
|
|
40
40
|
const piece = {};
|
|
41
41
|
await self.convert(req, req.body.data, piece);
|
|
42
42
|
|
|
43
|
+
// Re-annotate attachments so that relationship _fields
|
|
44
|
+
// (crop/focal-point) are reflected in _urls for CSS generation.
|
|
45
|
+
if (self.hasBackgroundFields) {
|
|
46
|
+
self.apos.attachment.all([ piece ], { annotate: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
req.res.setHeader('Content-Type', 'text/css');
|
|
44
50
|
|
|
45
51
|
return self.getStylesheet(piece);
|
|
@@ -23,6 +23,11 @@ node app @apostrophecms-pro/palette:migrate-to-styles
|
|
|
23
23
|
afterSave: {
|
|
24
24
|
async mirrorToGlobal(req, doc, options) {
|
|
25
25
|
// mirror the stylesheet to @apostrophecms/global
|
|
26
|
+
// Re-annotate attachments so that relationship _fields
|
|
27
|
+
// (crop/focal-point) are reflected in _urls for CSS generation.
|
|
28
|
+
if (self.hasBackgroundFields) {
|
|
29
|
+
self.apos.attachment.all([ doc ], { annotate: true });
|
|
30
|
+
}
|
|
26
31
|
const { css, classes } = self.getStylesheet(doc);
|
|
27
32
|
const $set = {
|
|
28
33
|
stylesStylesheet: css,
|
|
@@ -125,6 +125,8 @@ module.exports = (self, options) => {
|
|
|
125
125
|
});
|
|
126
126
|
}
|
|
127
127
|
if (!hasLink) {
|
|
128
|
+
// Prevent an XSS attack even if a styles exploit is found
|
|
129
|
+
const css = (req.data.global.stylesStylesheet || '').replaceAll('</', '<\\/');
|
|
128
130
|
nodes.push({
|
|
129
131
|
name: 'style',
|
|
130
132
|
attrs: {
|
|
@@ -132,7 +134,7 @@ module.exports = (self, options) => {
|
|
|
132
134
|
},
|
|
133
135
|
body: [
|
|
134
136
|
{
|
|
135
|
-
raw:
|
|
137
|
+
raw: css
|
|
136
138
|
}
|
|
137
139
|
]
|
|
138
140
|
});
|
|
@@ -156,7 +158,8 @@ module.exports = (self, options) => {
|
|
|
156
158
|
// to the <body> element (`class` attribute).
|
|
157
159
|
getStylesheet(doc) {
|
|
158
160
|
return self.stylesheetGlobalRender(self.schema, doc, {
|
|
159
|
-
checkIfConditionsFn: self.styleCheckIfConditions
|
|
161
|
+
checkIfConditionsFn: self.styleCheckIfConditions,
|
|
162
|
+
imageSizes: self.apos.attachment.imageSizes
|
|
160
163
|
});
|
|
161
164
|
},
|
|
162
165
|
// Returns object with `css` (string), `inline` (string) and `classes` (array)
|
|
@@ -172,7 +175,8 @@ module.exports = (self, options) => {
|
|
|
172
175
|
getWidgetStylesheet(schema, doc, options = {}) {
|
|
173
176
|
return self.stylesheetScopedRender(schema, doc, {
|
|
174
177
|
...options,
|
|
175
|
-
checkIfConditionsFn: self.styleCheckIfConditions
|
|
178
|
+
checkIfConditionsFn: self.styleCheckIfConditions,
|
|
179
|
+
imageSizes: self.apos.attachment.imageSizes
|
|
176
180
|
});
|
|
177
181
|
},
|
|
178
182
|
// Generate unique ID, Invoke the widget owned `getStylesheet` method
|
|
@@ -229,6 +233,8 @@ module.exports = (self, options) => {
|
|
|
229
233
|
transform: assetOptions.transform || null
|
|
230
234
|
});
|
|
231
235
|
}
|
|
236
|
+
// Prevent an XSS attack even if a styles exploit is found
|
|
237
|
+
css = css.replaceAll('</', '<\\/');
|
|
232
238
|
return `<style data-apos-widget-style-for="${widgetId}" data-apos-widget-style-id="${styleId}">\n` +
|
|
233
239
|
css +
|
|
234
240
|
'\n</style>';
|
|
@@ -179,6 +179,125 @@ module.exports = (moduleOptions) => {
|
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
|
+
},
|
|
183
|
+
background: {
|
|
184
|
+
label: 'apostrophe:styleBackground',
|
|
185
|
+
type: 'object',
|
|
186
|
+
customType: 'background',
|
|
187
|
+
options: {
|
|
188
|
+
flat: true,
|
|
189
|
+
hideLabel: true
|
|
190
|
+
},
|
|
191
|
+
property: '--preset-bg',
|
|
192
|
+
fields: {
|
|
193
|
+
add: {
|
|
194
|
+
enabled: {
|
|
195
|
+
label: 'apostrophe:styleBackground',
|
|
196
|
+
type: 'boolean',
|
|
197
|
+
def: false
|
|
198
|
+
},
|
|
199
|
+
backgroundType: {
|
|
200
|
+
label: 'apostrophe:styleBackgroundType',
|
|
201
|
+
type: 'select',
|
|
202
|
+
def: 'color',
|
|
203
|
+
if: { enabled: true },
|
|
204
|
+
choices: [
|
|
205
|
+
{
|
|
206
|
+
label: 'apostrophe:styleBackgroundColor',
|
|
207
|
+
value: 'color'
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
label: 'apostrophe:styleBackgroundGradient',
|
|
211
|
+
value: 'gradient'
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
label: 'apostrophe:styleBackgroundImage',
|
|
215
|
+
value: 'image'
|
|
216
|
+
}
|
|
217
|
+
]
|
|
218
|
+
},
|
|
219
|
+
color: {
|
|
220
|
+
label: 'apostrophe:styleBackgroundColor',
|
|
221
|
+
type: 'color',
|
|
222
|
+
if: {
|
|
223
|
+
enabled: true,
|
|
224
|
+
backgroundType: 'color'
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
gradientStart: {
|
|
228
|
+
label: 'apostrophe:styleGradientStart',
|
|
229
|
+
type: 'color',
|
|
230
|
+
def: '#000000',
|
|
231
|
+
if: {
|
|
232
|
+
enabled: true,
|
|
233
|
+
backgroundType: 'gradient'
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
gradientEnd: {
|
|
237
|
+
label: 'apostrophe:styleGradientEnd',
|
|
238
|
+
type: 'color',
|
|
239
|
+
def: '#ffffff',
|
|
240
|
+
if: {
|
|
241
|
+
enabled: true,
|
|
242
|
+
backgroundType: 'gradient'
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
gradientAngle: {
|
|
246
|
+
label: 'apostrophe:styleGradientAngle',
|
|
247
|
+
type: 'range',
|
|
248
|
+
min: 0,
|
|
249
|
+
max: 360,
|
|
250
|
+
step: 5,
|
|
251
|
+
def: 180,
|
|
252
|
+
unit: 'deg',
|
|
253
|
+
if: {
|
|
254
|
+
enabled: true,
|
|
255
|
+
backgroundType: 'gradient'
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
_image: {
|
|
259
|
+
label: 'apostrophe:styleBackgroundImage',
|
|
260
|
+
type: 'relationship',
|
|
261
|
+
withType: '@apostrophecms/image',
|
|
262
|
+
max: 1,
|
|
263
|
+
if: {
|
|
264
|
+
enabled: true,
|
|
265
|
+
backgroundType: 'image'
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
overlay: {
|
|
269
|
+
label: 'apostrophe:styleBackgroundOverlay',
|
|
270
|
+
type: 'boolean',
|
|
271
|
+
def: false,
|
|
272
|
+
if: {
|
|
273
|
+
enabled: true,
|
|
274
|
+
backgroundType: 'image'
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
overlayColor: {
|
|
278
|
+
label: 'apostrophe:styleOverlayColor',
|
|
279
|
+
type: 'color',
|
|
280
|
+
def: '#000000',
|
|
281
|
+
if: {
|
|
282
|
+
enabled: true,
|
|
283
|
+
backgroundType: 'image',
|
|
284
|
+
overlay: true
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
overlayOpacity: {
|
|
288
|
+
label: 'apostrophe:styleOverlayOpacity',
|
|
289
|
+
type: 'range',
|
|
290
|
+
min: 0,
|
|
291
|
+
max: 100,
|
|
292
|
+
def: 50,
|
|
293
|
+
if: {
|
|
294
|
+
enabled: true,
|
|
295
|
+
backgroundType: 'image',
|
|
296
|
+
overlay: true
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
182
301
|
}
|
|
183
302
|
};
|
|
184
303
|
};
|
|
@@ -85,9 +85,8 @@
|
|
|
85
85
|
// via alias `Modules/@apostrophecms/ui/schema/universal/check-if-conditions.mjs`.
|
|
86
86
|
// Replace the backend imports to import
|
|
87
87
|
// from `../path-to/@apostrophecms/ui/apos/schema/universal/check-if-conditions.mjs`.
|
|
88
|
-
import checkIfConditions from 'apostrophe/lib/universal/check-if-conditions.mjs';
|
|
89
88
|
import AposThemeMixin from 'Modules/@apostrophecms/ui/mixins/AposThemeMixin';
|
|
90
|
-
import renderCss from 'Modules/@apostrophecms/styles/
|
|
89
|
+
import { renderGlobalStyles as renderCss } from 'Modules/@apostrophecms/styles/render-factory.js';
|
|
91
90
|
import { klona } from 'klona';
|
|
92
91
|
import breakpointPreviewTransformer from 'postcss-viewport-to-container-toggle/standalone.js';
|
|
93
92
|
import { useDraggableWindow } from 'Modules/@apostrophecms/ui/composables/useDraggableWindow.js';
|
|
@@ -342,9 +341,7 @@ export default {
|
|
|
342
341
|
if (Object.keys(this.docFields.data).length === 0) {
|
|
343
342
|
return;
|
|
344
343
|
}
|
|
345
|
-
const { classes } = renderCss(this.schema, this.docFields.data
|
|
346
|
-
checkIfConditionsFn: checkIfConditions
|
|
347
|
-
});
|
|
344
|
+
const { classes } = renderCss(this.schema, this.docFields.data);
|
|
348
345
|
document.body.setAttribute(this.bodyAttr, classes.join(' '));
|
|
349
346
|
},
|
|
350
347
|
fillGroupData(group) {
|
|
@@ -425,9 +422,7 @@ export default {
|
|
|
425
422
|
},
|
|
426
423
|
async renderBrowserSide() {
|
|
427
424
|
await this.setStyleMarkup(
|
|
428
|
-
renderCss(this.schema, this.docFields.data
|
|
429
|
-
checkIfConditionsFn: checkIfConditions
|
|
430
|
-
})
|
|
425
|
+
renderCss(this.schema, this.docFields.data)
|
|
431
426
|
);
|
|
432
427
|
},
|
|
433
428
|
async setStyleMarkup({ css, classes }) {
|
|
@@ -3,8 +3,7 @@ import {
|
|
|
3
3
|
} from 'vue';
|
|
4
4
|
import { createId } from '@paralleldrive/cuid2';
|
|
5
5
|
import { isEqual } from 'lodash';
|
|
6
|
-
import { renderScopedStyles } from 'Modules/@apostrophecms/styles/
|
|
7
|
-
import checkIfConditions from 'apostrophe/lib/universal/check-if-conditions.mjs';
|
|
6
|
+
import { renderScopedStyles } from 'Modules/@apostrophecms/styles/render-factory.js';
|
|
8
7
|
import breakpointPreviewTransformer from 'postcss-viewport-to-container-toggle/standalone.js';
|
|
9
8
|
|
|
10
9
|
export function useAposStyles() {
|
|
@@ -28,7 +27,6 @@ export function useAposStyles() {
|
|
|
28
27
|
|
|
29
28
|
const styles = renderScopedStyles(schema, doc, {
|
|
30
29
|
rootSelector: `#${widgetId.value}`,
|
|
31
|
-
checkIfConditionsFn: checkIfConditions,
|
|
32
30
|
subset: stylesFields
|
|
33
31
|
});
|
|
34
32
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createRenderer } from './universal/render.mjs';
|
|
2
|
+
import checkIfConditions from 'apostrophe/lib/universal/check-if-conditions.mjs';
|
|
3
|
+
|
|
4
|
+
let instance = null;
|
|
5
|
+
|
|
6
|
+
export function getRenderer(overrides = {}) {
|
|
7
|
+
if (Object.keys(overrides).length) {
|
|
8
|
+
return createRenderer({
|
|
9
|
+
checkIfConditionsFn: checkIfConditions,
|
|
10
|
+
imageSizes: window.apos.attachment.imageSizes,
|
|
11
|
+
...overrides
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
if (!instance) {
|
|
15
|
+
instance = createRenderer({
|
|
16
|
+
checkIfConditionsFn: checkIfConditions,
|
|
17
|
+
imageSizes: window.apos.attachment.imageSizes
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return instance;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function renderGlobalStyles(schema, doc, options = {}) {
|
|
24
|
+
return getRenderer().renderGlobalStyles(schema, doc, options);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function renderScopedStyles(schema, doc, options = {}) {
|
|
28
|
+
return getRenderer().renderScopedStyles(schema, doc, options);
|
|
29
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Hardcoded fallback widths — used only when imageSizes is not provided
|
|
2
|
+
const KNOWN_SIZE_WIDTHS = {
|
|
3
|
+
'one-sixth': 190,
|
|
4
|
+
'one-third': 380,
|
|
5
|
+
'one-half': 570,
|
|
6
|
+
'two-thirds': 760,
|
|
7
|
+
full: 1140,
|
|
8
|
+
max: 1600
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function extractImageData(value) {
|
|
12
|
+
if (!value || value.group !== 'images') {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const urls = value._urls;
|
|
17
|
+
if (!urls || !Object.keys(urls).length) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return urls;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getSizeWidths(imageSizes) {
|
|
25
|
+
if (!Array.isArray(imageSizes) || !imageSizes.length) {
|
|
26
|
+
return KNOWN_SIZE_WIDTHS;
|
|
27
|
+
}
|
|
28
|
+
const map = {};
|
|
29
|
+
for (const size of imageSizes) {
|
|
30
|
+
map[size.name] = size.width;
|
|
31
|
+
}
|
|
32
|
+
return map;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Assume 2× device-pixel-ratio when selecting images for a
|
|
36
|
+
// breakpoint. Covers the vast majority of modern displays
|
|
37
|
+
// (retina / HiDPI) without needing resolution media queries.
|
|
38
|
+
const DPR_FACTOR = 2;
|
|
39
|
+
|
|
40
|
+
// Internal breakpoints for responsive background images.
|
|
41
|
+
// Sorted ascending — each breakpoint generates a non-overlapping
|
|
42
|
+
// range query so there is no cascade/specificity dependency.
|
|
43
|
+
const BREAKPOINTS = [
|
|
44
|
+
{
|
|
45
|
+
maxWidth: 480,
|
|
46
|
+
label: 'mobile'
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
maxWidth: 768,
|
|
50
|
+
label: 'tablet'
|
|
51
|
+
}
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// For a given breakpoint width, find the best image URL.
|
|
55
|
+
// Multiplies the target by DPR_FACTOR so that retina/HiDPI
|
|
56
|
+
// displays get enough pixel data for crisp rendering.
|
|
57
|
+
// Falls back to the largest available image when nothing qualifies.
|
|
58
|
+
function bestUrlForWidth(entries, targetWidth) {
|
|
59
|
+
const adjusted = targetWidth * DPR_FACTOR;
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
if (entry.width >= adjusted) {
|
|
62
|
+
return entry.url;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return entries[entries.length - 1]?.url || null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function buildResponsiveImageRules(property, urls, imageSizes) {
|
|
70
|
+
const sizeWidths = getSizeWidths(imageSizes);
|
|
71
|
+
|
|
72
|
+
// Build sorted entries ascending by width
|
|
73
|
+
const entries = [];
|
|
74
|
+
for (const [ name, sizedUrl ] of Object.entries(urls)) {
|
|
75
|
+
if (
|
|
76
|
+
name === 'original' ||
|
|
77
|
+
name === 'uncropped' ||
|
|
78
|
+
!sizedUrl ||
|
|
79
|
+
typeof sizedUrl !== 'string'
|
|
80
|
+
) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const width = sizeWidths[name];
|
|
84
|
+
if (width) {
|
|
85
|
+
entries.push({
|
|
86
|
+
url: sizedUrl,
|
|
87
|
+
width
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
entries.sort((a, b) => a.width - b.width);
|
|
92
|
+
|
|
93
|
+
// Use the largest sized image as the base (covers the widest viewport).
|
|
94
|
+
// Fall back to `original` when no sized entries exist (e.g. SVG).
|
|
95
|
+
const baseUrl = entries.length
|
|
96
|
+
? entries[entries.length - 1].url
|
|
97
|
+
: urls.original;
|
|
98
|
+
|
|
99
|
+
if (!baseUrl) {
|
|
100
|
+
return {
|
|
101
|
+
rules: [],
|
|
102
|
+
mediaRules: []
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const rules = [ `${property}: url(${baseUrl})` ];
|
|
107
|
+
const mediaRules = [];
|
|
108
|
+
|
|
109
|
+
if (entries.length > 1) {
|
|
110
|
+
for (let i = 0; i < BREAKPOINTS.length; i++) {
|
|
111
|
+
const bp = BREAKPOINTS[i];
|
|
112
|
+
const bpUrl = bestUrlForWidth(entries, bp.maxWidth);
|
|
113
|
+
// Skip if the breakpoint would use the same URL as the base —
|
|
114
|
+
// the base rule already covers it at every viewport.
|
|
115
|
+
if (bpUrl && bpUrl !== baseUrl) {
|
|
116
|
+
// First breakpoint: (width <= Npx)
|
|
117
|
+
// Subsequent: (Ppx < width <= Npx)
|
|
118
|
+
const query = i === 0
|
|
119
|
+
? `(width <= ${bp.maxWidth}px)`
|
|
120
|
+
: `(${BREAKPOINTS[i - 1].maxWidth}px < width <= ${bp.maxWidth}px)`;
|
|
121
|
+
mediaRules.push({
|
|
122
|
+
query,
|
|
123
|
+
rules: [ `${property}: url(${bpUrl})` ]
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
rules,
|
|
131
|
+
mediaRules
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function hexToRgba(hex, opacity) {
|
|
136
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
137
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
138
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
139
|
+
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
|
140
|
+
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extractImageData,
|
|
3
|
+
buildResponsiveImageRules,
|
|
4
|
+
hexToRgba
|
|
5
|
+
} from './backgroundHelpers.mjs';
|
|
6
|
+
|
|
1
7
|
export default {
|
|
2
8
|
/**
|
|
3
9
|
* Custom render functions for fields with nuanced data structures
|
|
@@ -60,5 +66,104 @@ export default {
|
|
|
60
66
|
field,
|
|
61
67
|
rule
|
|
62
68
|
};
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
background: function ({
|
|
72
|
+
field, subfields, options
|
|
73
|
+
}) {
|
|
74
|
+
const varBase = field.raw.property || '--preset-bg';
|
|
75
|
+
const bgType = subfields.backgroundType?.value || 'color';
|
|
76
|
+
|
|
77
|
+
const PROCESSED = [
|
|
78
|
+
'enabled', 'backgroundType',
|
|
79
|
+
'color',
|
|
80
|
+
'gradientStart', 'gradientEnd', 'gradientAngle',
|
|
81
|
+
'_image',
|
|
82
|
+
'overlay', 'overlayColor', 'overlayOpacity'
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
// Color Mode
|
|
86
|
+
if (bgType === 'color') {
|
|
87
|
+
const color = subfields.color?.value;
|
|
88
|
+
if (!color) {
|
|
89
|
+
return {
|
|
90
|
+
field,
|
|
91
|
+
rules: [],
|
|
92
|
+
processedFields: PROCESSED
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
field,
|
|
97
|
+
rules: [ `background-color: ${color}` ],
|
|
98
|
+
processedFields: PROCESSED
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Gradient Mode
|
|
103
|
+
if (bgType === 'gradient') {
|
|
104
|
+
const start = subfields.gradientStart?.value || '#000000';
|
|
105
|
+
const end = subfields.gradientEnd?.value || '#ffffff';
|
|
106
|
+
const angle = subfields.gradientAngle?.value ?? 180;
|
|
107
|
+
const unit = subfields.gradientAngle?.unit || 'deg';
|
|
108
|
+
return {
|
|
109
|
+
field,
|
|
110
|
+
rules: [ `background: linear-gradient(${angle}${unit}, ${start}, ${end})` ],
|
|
111
|
+
processedFields: PROCESSED
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Image Mode
|
|
116
|
+
const rel = subfields._image?.value;
|
|
117
|
+
const urls = extractImageData(rel?.[0]?.attachment);
|
|
118
|
+
if (!urls) {
|
|
119
|
+
return {
|
|
120
|
+
field,
|
|
121
|
+
rules: [],
|
|
122
|
+
processedFields: PROCESSED
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const rules = [];
|
|
127
|
+
|
|
128
|
+
// --- CSS Variable Export ---
|
|
129
|
+
const responsive = buildResponsiveImageRules(
|
|
130
|
+
`${varBase}-image`, urls, options.imageSizes
|
|
131
|
+
);
|
|
132
|
+
rules.push(...responsive.rules);
|
|
133
|
+
|
|
134
|
+
// --- Overlay ---
|
|
135
|
+
const layers = [];
|
|
136
|
+
const overlay = subfields.overlay?.value;
|
|
137
|
+
const overlayColor = subfields.overlayColor?.value || '#000000';
|
|
138
|
+
const overlayOpacity = (subfields.overlayOpacity?.value ?? 50) / 100;
|
|
139
|
+
|
|
140
|
+
if (overlay && overlayColor) {
|
|
141
|
+
const rgba = hexToRgba(overlayColor, overlayOpacity);
|
|
142
|
+
rules.push(`${varBase}-overlay: linear-gradient(${rgba}, ${rgba})`);
|
|
143
|
+
layers.push(`var(${varBase}-overlay-layer, var(${varBase}-overlay))`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- Override Hook Reset ---
|
|
147
|
+
// Reset inherited hook values so nested widgets aren't affected
|
|
148
|
+
// by a parent's blur recipe setting these to `none`.
|
|
149
|
+
rules.push(`${varBase}-image-layer: initial`);
|
|
150
|
+
rules.push(`${varBase}-overlay-layer: initial`);
|
|
151
|
+
|
|
152
|
+
// --- Image Layer ---
|
|
153
|
+
const imageFallback =
|
|
154
|
+
`var(${varBase}-image) center / cover no-repeat`;
|
|
155
|
+
layers.push(`var(${varBase}-image-layer, ${imageFallback})`);
|
|
156
|
+
|
|
157
|
+
// --- Composite background shorthand ---
|
|
158
|
+
rules.push(`background: ${layers.join(', ')}`);
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
field,
|
|
162
|
+
rules,
|
|
163
|
+
processedFields: PROCESSED,
|
|
164
|
+
...responsive.mediaRules.length > 0 && {
|
|
165
|
+
mediaRules: responsive.mediaRules
|
|
166
|
+
}
|
|
167
|
+
};
|
|
63
168
|
}
|
|
64
169
|
};
|