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
|
@@ -57,8 +57,37 @@
|
|
|
57
57
|
import { klona } from 'klona';
|
|
58
58
|
import customRules from './customRules.mjs';
|
|
59
59
|
|
|
60
|
+
// For legacy reasons this stays the default export.
|
|
60
61
|
export default renderGlobalStyles;
|
|
61
|
-
export {
|
|
62
|
+
export {
|
|
63
|
+
renderGlobalStyles, renderScopedStyles, createRenderer
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Creates a renderer with shared options pre-applied.
|
|
68
|
+
* Callers can override per-call options as needed.
|
|
69
|
+
*
|
|
70
|
+
* @param {Object} options - Shared options for all render calls
|
|
71
|
+
* @returns {{ renderGlobalStyles: Function, renderScopedStyles: Function }}
|
|
72
|
+
*/
|
|
73
|
+
function createRenderer(options = {}) {
|
|
74
|
+
const memoized = { ...options };
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
renderGlobalStyles(schema, doc, callOptions = {}) {
|
|
78
|
+
return renderGlobalStyles(schema, doc, {
|
|
79
|
+
...memoized,
|
|
80
|
+
...callOptions
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
renderScopedStyles(schema, doc, callOptions = {}) {
|
|
84
|
+
return renderScopedStyles(schema, doc, {
|
|
85
|
+
...memoized,
|
|
86
|
+
...callOptions
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
62
91
|
|
|
63
92
|
// Exported for testing purposes
|
|
64
93
|
export const NORMALIZERS = {
|
|
@@ -67,7 +96,8 @@ export const NORMALIZERS = {
|
|
|
67
96
|
};
|
|
68
97
|
export const EXTRACTORS = {
|
|
69
98
|
_: extract,
|
|
70
|
-
object: extractObject
|
|
99
|
+
object: extractObject,
|
|
100
|
+
customType: extractCustomType
|
|
71
101
|
};
|
|
72
102
|
const FILTERS = {
|
|
73
103
|
_: filter,
|
|
@@ -85,7 +115,8 @@ const FILTERS = {
|
|
|
85
115
|
* @returns {{ css: string; classes: string[] }} Compiled CSS stylesheet and classes
|
|
86
116
|
*/
|
|
87
117
|
function renderGlobalStyles(schema, doc, {
|
|
88
|
-
checkIfConditionsFn
|
|
118
|
+
checkIfConditionsFn,
|
|
119
|
+
...engineOptions
|
|
89
120
|
} = {}) {
|
|
90
121
|
const withConditions = filterConditionalFields(
|
|
91
122
|
klona(schema),
|
|
@@ -97,7 +128,8 @@ function renderGlobalStyles(schema, doc, {
|
|
|
97
128
|
const storage = {
|
|
98
129
|
classes: new Set(),
|
|
99
130
|
styles: new Map(),
|
|
100
|
-
conditions: withConditions.conditions
|
|
131
|
+
conditions: withConditions.conditions,
|
|
132
|
+
options: engineOptions
|
|
101
133
|
};
|
|
102
134
|
|
|
103
135
|
for (const field of withConditions.schema) {
|
|
@@ -105,6 +137,10 @@ function renderGlobalStyles(schema, doc, {
|
|
|
105
137
|
if (!filter(field, doc)) {
|
|
106
138
|
continue;
|
|
107
139
|
}
|
|
140
|
+
if (field.customType) {
|
|
141
|
+
extractCustomType(field, doc, { storage });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
108
144
|
const normalizer = NORMALIZERS[field.type] || NORMALIZERS._;
|
|
109
145
|
const extractor = EXTRACTORS[field.type] || EXTRACTORS._;
|
|
110
146
|
const normalzied = normalizer(field, doc, {
|
|
@@ -135,7 +171,8 @@ function renderGlobalStyles(schema, doc, {
|
|
|
135
171
|
function renderScopedStyles(schema, doc, {
|
|
136
172
|
rootSelector = null,
|
|
137
173
|
checkIfConditionsFn,
|
|
138
|
-
subset = null
|
|
174
|
+
subset = null,
|
|
175
|
+
...engineOptions
|
|
139
176
|
} = {}) {
|
|
140
177
|
const withConditions = filterConditionalFields(
|
|
141
178
|
klona(schema),
|
|
@@ -149,7 +186,8 @@ function renderScopedStyles(schema, doc, {
|
|
|
149
186
|
classes: new Set(),
|
|
150
187
|
styles: new Map(),
|
|
151
188
|
inlineVotes: new Set(),
|
|
152
|
-
conditions: withConditions.conditions
|
|
189
|
+
conditions: withConditions.conditions,
|
|
190
|
+
options: engineOptions
|
|
153
191
|
};
|
|
154
192
|
|
|
155
193
|
for (const field of withConditions.schema) {
|
|
@@ -157,6 +195,13 @@ function renderScopedStyles(schema, doc, {
|
|
|
157
195
|
if (!filter(field, doc)) {
|
|
158
196
|
continue;
|
|
159
197
|
}
|
|
198
|
+
if (field.customType) {
|
|
199
|
+
extractCustomType(field, doc, {
|
|
200
|
+
rootSelector,
|
|
201
|
+
storage
|
|
202
|
+
});
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
160
205
|
const normalizer = NORMALIZERS[field.type] || NORMALIZERS._;
|
|
161
206
|
const extractor = EXTRACTORS[field.type] || EXTRACTORS._;
|
|
162
207
|
const normalized = normalizer(field, doc, {
|
|
@@ -326,6 +371,9 @@ function filter(field, doc) {
|
|
|
326
371
|
if (!doc[field.name] && doc[field.name] !== 0) {
|
|
327
372
|
return false;
|
|
328
373
|
}
|
|
374
|
+
if (field.customType) {
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
329
377
|
const hasProperty = Array.isArray(field.property)
|
|
330
378
|
? field.property.length > 0
|
|
331
379
|
: !!field.property;
|
|
@@ -356,6 +404,9 @@ function filterObject(field, doc) {
|
|
|
356
404
|
if (!doc[field.name]) {
|
|
357
405
|
return false;
|
|
358
406
|
}
|
|
407
|
+
if (field.customType) {
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
359
410
|
if (field.property && field.valueTemplate) {
|
|
360
411
|
return true;
|
|
361
412
|
}
|
|
@@ -390,17 +441,33 @@ function normalize(field, doc, {
|
|
|
390
441
|
const fieldUnit = field.unit || '';
|
|
391
442
|
const fieldMediaQuery = field.mediaQuery || rootMediaQuery;
|
|
392
443
|
|
|
393
|
-
|
|
394
|
-
|
|
444
|
+
// skipFalsyValues: when enabled and value is falsy, skip the field entirely.
|
|
445
|
+
// No CSS property, no CSS variable, no class toggle — nothing is emitted.
|
|
446
|
+
if (field.skipFalsyValues && !fieldValue) {
|
|
395
447
|
return {
|
|
396
448
|
raw: field,
|
|
397
|
-
selectors,
|
|
398
|
-
properties,
|
|
399
|
-
value:
|
|
449
|
+
selectors: [],
|
|
450
|
+
properties: [],
|
|
451
|
+
value: null,
|
|
400
452
|
unit: ''
|
|
401
453
|
};
|
|
402
454
|
}
|
|
403
455
|
|
|
456
|
+
if (field.class) {
|
|
457
|
+
applyFieldClass(field.class, fieldValue, storage);
|
|
458
|
+
// When the field also has `property`, continue to generate CSS output.
|
|
459
|
+
// Otherwise return early (class-only field, preserving existing behavior).
|
|
460
|
+
if (!field.property) {
|
|
461
|
+
return {
|
|
462
|
+
raw: field,
|
|
463
|
+
selectors,
|
|
464
|
+
properties,
|
|
465
|
+
value: fieldValue,
|
|
466
|
+
unit: ''
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
404
471
|
if (!properties) {
|
|
405
472
|
properties = [];
|
|
406
473
|
}
|
|
@@ -485,9 +552,11 @@ function normalizeObject(field, doc, {
|
|
|
485
552
|
});
|
|
486
553
|
delete normalized.unit;
|
|
487
554
|
|
|
488
|
-
const schema = field.
|
|
489
|
-
|
|
490
|
-
|
|
555
|
+
const schema = field.customType
|
|
556
|
+
? field.schema
|
|
557
|
+
: field.schema.filter(subfield => {
|
|
558
|
+
return filter(subfield, doc[field.name] || {});
|
|
559
|
+
});
|
|
491
560
|
|
|
492
561
|
for (const subfield of schema) {
|
|
493
562
|
subfields.push(
|
|
@@ -518,7 +587,7 @@ function normalizeObject(field, doc, {
|
|
|
518
587
|
* @param {RuntimeStorage} storage
|
|
519
588
|
*/
|
|
520
589
|
function extract(normalized, storage) {
|
|
521
|
-
if (normalized.class) {
|
|
590
|
+
if (normalized.class && normalized.properties.length === 0) {
|
|
522
591
|
return;
|
|
523
592
|
}
|
|
524
593
|
const styles = storage.styles;
|
|
@@ -570,6 +639,117 @@ function extract(normalized, storage) {
|
|
|
570
639
|
});
|
|
571
640
|
}
|
|
572
641
|
|
|
642
|
+
/**
|
|
643
|
+
* Dispatch a field with a `customType` to the matching custom rule.
|
|
644
|
+
* The custom rule is authoritative — the field doesn't flow through
|
|
645
|
+
* the standard normalize → extract pipeline.
|
|
646
|
+
*
|
|
647
|
+
* For object fields: normalizes with normalizeObject(), builds a subfield
|
|
648
|
+
* map, and lets the rule do partial processing (remaining subfields
|
|
649
|
+
* continue through extract()).
|
|
650
|
+
*
|
|
651
|
+
* For non-object fields: normalizes normally, passes an empty subfield
|
|
652
|
+
* map, and the rule is fully responsible for CSS output.
|
|
653
|
+
*
|
|
654
|
+
* Custom rules may return `inline: false` to explicitly opt out of
|
|
655
|
+
* inline styles when their output requires scoped CSS (e.g.
|
|
656
|
+
* pseudo-element rules). When absent, the field-level vote stands.
|
|
657
|
+
*
|
|
658
|
+
* @param {SchemaField} field
|
|
659
|
+
* @param {Object} doc
|
|
660
|
+
* @param {Object} opts
|
|
661
|
+
* @param {String} [opts.rootSelector]
|
|
662
|
+
* @param {RuntimeStorage} opts.storage
|
|
663
|
+
*/
|
|
664
|
+
function extractCustomType(field, doc, { rootSelector, storage } = {}) {
|
|
665
|
+
const ruleName = field.customType;
|
|
666
|
+
const rule = customRules[ruleName];
|
|
667
|
+
if (!rule) {
|
|
668
|
+
|
|
669
|
+
console.error(
|
|
670
|
+
`[styles] Unknown customType "${ruleName}" on field "${field.name}". ` +
|
|
671
|
+
'The field will be skipped. Available types: ' +
|
|
672
|
+
Object.keys(customRules).join(', ')
|
|
673
|
+
);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const isObject = field.type === 'object';
|
|
678
|
+
const normalizer = isObject ? normalizeObject : normalize;
|
|
679
|
+
const normalized = normalizer(field, doc, {
|
|
680
|
+
rootSelector,
|
|
681
|
+
storage
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
const subfieldMap = {};
|
|
685
|
+
if (isObject && normalized.subfields) {
|
|
686
|
+
for (const sub of normalized.subfields) {
|
|
687
|
+
subfieldMap[sub.raw.name] = sub;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const result = rule({
|
|
692
|
+
field: normalized,
|
|
693
|
+
subfields: subfieldMap,
|
|
694
|
+
options: storage.options || {}
|
|
695
|
+
});
|
|
696
|
+
const processed = new Set(result.processedFields || []);
|
|
697
|
+
|
|
698
|
+
if (result.inline === false && storage.inlineVotes) {
|
|
699
|
+
storage.inlineVotes.add(false);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Add the custom rule's CSS output to storage
|
|
703
|
+
if (result.rules?.length) {
|
|
704
|
+
for (const selector of normalized.selectors) {
|
|
705
|
+
let currentStyles = storage.styles;
|
|
706
|
+
if (normalized.mediaQuery) {
|
|
707
|
+
const mediaQuery = `@media ${normalized.mediaQuery}`;
|
|
708
|
+
storage.styles.set(mediaQuery, storage.styles.get(mediaQuery) || new Map());
|
|
709
|
+
currentStyles = storage.styles.get(mediaQuery);
|
|
710
|
+
}
|
|
711
|
+
for (const r of result.rules) {
|
|
712
|
+
currentStyles.set(
|
|
713
|
+
selector, (currentStyles.get(selector) || new Set()).add(r)
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Add media-scoped rules from the custom rule (e.g. responsive image breakpoints).
|
|
720
|
+
// Media rules require scoped CSS, so automatically opt out of inline styles.
|
|
721
|
+
if (result.mediaRules?.length) {
|
|
722
|
+
if (storage.inlineVotes) {
|
|
723
|
+
storage.inlineVotes.add(false);
|
|
724
|
+
}
|
|
725
|
+
for (const mr of result.mediaRules) {
|
|
726
|
+
const mediaQuery = `@media ${mr.query}`;
|
|
727
|
+
storage.styles.set(mediaQuery, storage.styles.get(mediaQuery) || new Map());
|
|
728
|
+
const mediaStyles = storage.styles.get(mediaQuery);
|
|
729
|
+
for (const selector of normalized.selectors) {
|
|
730
|
+
for (const r of mr.rules) {
|
|
731
|
+
mediaStyles.set(
|
|
732
|
+
selector, (mediaStyles.get(selector) || new Set()).add(r)
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// For object fields: continue processing subfields NOT consumed by the rule.
|
|
740
|
+
// Filter before extract — normalizeObject deliberately skips filtering for
|
|
741
|
+
// customType so the rule receives the full schema. Leftover subfields must
|
|
742
|
+
// pass the same filter the main loop applies to avoid emitting broken CSS.
|
|
743
|
+
if (isObject && normalized.subfields) {
|
|
744
|
+
const subdoc = doc[field.name] || {};
|
|
745
|
+
for (const sub of normalized.subfields) {
|
|
746
|
+
if (!processed.has(sub.raw.name) && filter(sub.raw, subdoc)) {
|
|
747
|
+
extract(sub, storage);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
573
753
|
/**
|
|
574
754
|
* Extract CSS rules from a normalized object field and populate the central
|
|
575
755
|
* styles map with the selectors and corresponding rules.
|
|
@@ -35,6 +35,7 @@ const path = require('path');
|
|
|
35
35
|
const { stripIndent } = require('common-tags');
|
|
36
36
|
const { SemanticAttributes } = require('@opentelemetry/semantic-conventions');
|
|
37
37
|
const voidElements = require('void-elements');
|
|
38
|
+
const safeJsonForScript = require('../../../lib/safe-json-script');
|
|
38
39
|
|
|
39
40
|
module.exports = {
|
|
40
41
|
options: { alias: 'template' },
|
|
@@ -1140,12 +1141,23 @@ module.exports = {
|
|
|
1140
1141
|
// attrs: { href: '/some/path', rel: 'stylesheet' }
|
|
1141
1142
|
// }
|
|
1142
1143
|
// ]
|
|
1143
|
-
// Node object SHOULD have either `name`, `text`, `raw
|
|
1144
|
-
// A node with `name` can have `attrs` (array of
|
|
1145
|
-
// and `body` (array of child nodes, recursion).
|
|
1144
|
+
// Node object SHOULD have either `name`, `text`, `raw`, `json` or
|
|
1145
|
+
// `comment` property. A node with `name` can have `attrs` (array of
|
|
1146
|
+
// element attributes) and `body` (array of child nodes, recursion).
|
|
1146
1147
|
// `text` nodes are rendered as text (no HTML tags), the value is always a string.
|
|
1147
1148
|
// `comment` nodes are rendered as HTML comments, the value is always a string.
|
|
1148
1149
|
// `raw` nodes are rendered as is, no escaping, the value is always a string.
|
|
1150
|
+
// `json` nodes are rendered as a JSON serialization of the value,
|
|
1151
|
+
// safely escaped for inclusion inside a `<script>` element so that
|
|
1152
|
+
// untrusted content cannot break out of the surrounding script tag.
|
|
1153
|
+
// Use this instead of building a `raw` body from `JSON.stringify()`
|
|
1154
|
+
// yourself, e.g.:
|
|
1155
|
+
//
|
|
1156
|
+
// {
|
|
1157
|
+
// name: 'script',
|
|
1158
|
+
// attrs: { type: 'application/ld+json' },
|
|
1159
|
+
// body: [ { json: myData } ]
|
|
1160
|
+
// }
|
|
1149
1161
|
renderNodes(nodes) {
|
|
1150
1162
|
if (!Array.isArray(nodes)) {
|
|
1151
1163
|
self.logError(
|
|
@@ -1164,6 +1176,9 @@ module.exports = {
|
|
|
1164
1176
|
if (node.raw != null) {
|
|
1165
1177
|
return node.raw;
|
|
1166
1178
|
}
|
|
1179
|
+
if (node.json != null) {
|
|
1180
|
+
return safeJsonForScript(node.json);
|
|
1181
|
+
}
|
|
1167
1182
|
if (node.name != null) {
|
|
1168
1183
|
const name = self.apos.util.escapeHtml(node.name);
|
|
1169
1184
|
const attrs = Object.entries(node.attrs || {})
|
|
@@ -1252,9 +1267,10 @@ module.exports = {
|
|
|
1252
1267
|
return data;
|
|
1253
1268
|
},
|
|
1254
1269
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1270
|
+
// An opportunity to modify `data` IN PLACE to send less data to
|
|
1271
|
+
// the external front, e.g. Astro. Invoked immediately before
|
|
1272
|
+
// data is sent to Astro
|
|
1273
|
+
pruneDataForExternalFront(req, template, data, moduleName) {},
|
|
1258
1274
|
|
|
1259
1275
|
getDocsForExternalFront(req, template, data, moduleName) {
|
|
1260
1276
|
return [
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
:show-restore="options.showRestore"
|
|
17
17
|
:show-dismiss-submission="options.showDismissSubmission"
|
|
18
18
|
:can-delete-draft="options.canDeleteDraft"
|
|
19
|
+
:show-unpublish="options.showUnpublish"
|
|
20
|
+
:cross-locale="options.crossLocale || false"
|
|
19
21
|
@menu-open="menuOpen = true"
|
|
20
22
|
@menu-close="menuOpen = false"
|
|
21
23
|
/>
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
class="apos-context-menu__dropdown-content"
|
|
42
42
|
:class="popoverClass"
|
|
43
43
|
tabindex="0"
|
|
44
|
-
@
|
|
44
|
+
@keydown.tab="onKeydownTab"
|
|
45
45
|
@keyup.esc="onKeyup"
|
|
46
46
|
@keyup.enter="onKeyup"
|
|
47
47
|
>
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
...popoverClass
|
|
76
76
|
]"
|
|
77
77
|
tabindex="0"
|
|
78
|
-
@
|
|
78
|
+
@keydown.tab="onKeydownTab"
|
|
79
79
|
@keyup.esc="onKeyup"
|
|
80
80
|
@keyup.enter="onKeyup"
|
|
81
81
|
>
|
|
@@ -559,11 +559,26 @@ const ignoreInputTypes = [
|
|
|
559
559
|
'week'
|
|
560
560
|
];
|
|
561
561
|
|
|
562
|
+
// Handle Tab on keydown — before the browser moves focus.
|
|
563
|
+
// At keydown time document.activeElement is the element the user
|
|
564
|
+
// is on (the source), and preventDefault() can stop the browser's
|
|
565
|
+
// default focus movement.
|
|
566
|
+
function onKeydownTab(event) {
|
|
567
|
+
if (!isOpen.value || modalDepth.value !== modalStore.getDepth()) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (event.target?.nodeName?.toLowerCase() === 'textarea') {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
event.stopImmediatePropagation();
|
|
574
|
+
onTab(event);
|
|
575
|
+
}
|
|
576
|
+
|
|
562
577
|
/**
|
|
563
578
|
* @param {KeyboardEvent} event
|
|
564
579
|
*/
|
|
565
580
|
function onKeyup(event) {
|
|
566
|
-
if (modalDepth.value !== modalStore.getDepth()
|
|
581
|
+
if (!isOpen.value || modalDepth.value !== modalStore.getDepth()) {
|
|
567
582
|
return;
|
|
568
583
|
}
|
|
569
584
|
|
|
@@ -572,7 +587,6 @@ function onKeyup(event) {
|
|
|
572
587
|
|
|
573
588
|
if (event.key === 'Tab' && target?.nodeName?.toLowerCase() !== 'textarea') {
|
|
574
589
|
event.stopImmediatePropagation();
|
|
575
|
-
onTab(event);
|
|
576
590
|
return;
|
|
577
591
|
}
|
|
578
592
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { onBeforeUnmount, unref } from 'vue';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('vue').Ref<HTMLElement>} ElementRef
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Reactive infinite scroll via IntersectionObserver.
|
|
9
|
+
* Observes a sentinel element and calls `onLoadMore` when it enters
|
|
10
|
+
* the visible area of the scroll container.
|
|
11
|
+
* Automatically disconnects the observer on component unmount.
|
|
12
|
+
* Handles edge cases when the results are shorter than the viewport,
|
|
13
|
+
* with exposed `recheck()` method to force a fresh intersection check after
|
|
14
|
+
* content changes. See `AposRecentlyEditedManager.vue` for an implementation
|
|
15
|
+
* of infinite loading with rechecks after appending items.
|
|
16
|
+
*
|
|
17
|
+
* @param {ElementRef} sentinel
|
|
18
|
+
* Template ref for the observed element
|
|
19
|
+
* @param {() => Promise<void>|void} onLoadMore
|
|
20
|
+
* Called when sentinel is visible; the caller is responsible
|
|
21
|
+
* for its own guards (e.g. "already loading")
|
|
22
|
+
* @param {{ rootMargin?: string, root?: ElementRef|string }} [options]
|
|
23
|
+
* `root`: Ref to the scroll container element, or a CSS selector
|
|
24
|
+
* string resolved relative to the sentinel's ancestors.
|
|
25
|
+
* Defaults to the viewport when omitted.
|
|
26
|
+
* @returns {{ start: () => void, stop: () => void }}
|
|
27
|
+
*/
|
|
28
|
+
export function useInfiniteScroll(sentinel, onLoadMore, options = {}) {
|
|
29
|
+
const { rootMargin = '100px', root = null } = options;
|
|
30
|
+
let observer = null;
|
|
31
|
+
|
|
32
|
+
function handleIntersect(entries) {
|
|
33
|
+
if (entries[0]?.isIntersecting) {
|
|
34
|
+
onLoadMore();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveRoot(sentinelEl) {
|
|
39
|
+
const raw = unref(root);
|
|
40
|
+
if (raw instanceof HTMLElement) {
|
|
41
|
+
return raw;
|
|
42
|
+
}
|
|
43
|
+
// CSS selector: walk up from sentinel to find the scroll container.
|
|
44
|
+
if (typeof raw === 'string' && sentinelEl) {
|
|
45
|
+
return sentinelEl.closest(raw) || null;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function start() {
|
|
51
|
+
stop();
|
|
52
|
+
const el = unref(sentinel);
|
|
53
|
+
if (!el) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const rootEl = resolveRoot(el);
|
|
57
|
+
observer = new IntersectionObserver(handleIntersect, {
|
|
58
|
+
root: rootEl,
|
|
59
|
+
rootMargin,
|
|
60
|
+
threshold: 0
|
|
61
|
+
});
|
|
62
|
+
observer.observe(el);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function stop() {
|
|
66
|
+
if (observer) {
|
|
67
|
+
observer.disconnect();
|
|
68
|
+
observer = null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
onBeforeUnmount(stop);
|
|
73
|
+
|
|
74
|
+
// Force a fresh intersection check. Useful after content changes
|
|
75
|
+
// (e.g. items appended) when the sentinel may already be visible
|
|
76
|
+
// but no state transition occurred.
|
|
77
|
+
function recheck() {
|
|
78
|
+
const el = unref(sentinel);
|
|
79
|
+
if (!observer || !el) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
observer.unobserve(el);
|
|
83
|
+
observer.observe(el);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
start,
|
|
88
|
+
stop,
|
|
89
|
+
recheck
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -128,6 +128,9 @@ export const useModalStore = defineStore('modal', () => {
|
|
|
128
128
|
props = await apos.ui.transformers[transformer](props);
|
|
129
129
|
}
|
|
130
130
|
return new Promise((resolve) => {
|
|
131
|
+
const modalLocale = props?.locale ||
|
|
132
|
+
activeModal.value?.locale ||
|
|
133
|
+
apos.i18n.locale;
|
|
131
134
|
const item = {
|
|
132
135
|
id: `modal:${createId()}`,
|
|
133
136
|
componentName,
|
|
@@ -135,11 +138,11 @@ export const useModalStore = defineStore('modal', () => {
|
|
|
135
138
|
props: props || {},
|
|
136
139
|
elementsToFocus: [],
|
|
137
140
|
focusedElement: null,
|
|
138
|
-
locale:
|
|
141
|
+
locale: modalLocale,
|
|
139
142
|
hasContextLocale: activeModal.value
|
|
140
143
|
? (activeModal.value.hasContextLocale ||
|
|
141
144
|
activeModal.value.locale !== apos.i18n.locale)
|
|
142
|
-
:
|
|
145
|
+
: modalLocale !== apos.i18n.locale
|
|
143
146
|
};
|
|
144
147
|
|
|
145
148
|
activeId.value = item.id;
|
|
@@ -176,6 +176,13 @@ class Queue {
|
|
|
176
176
|
});
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
clear() {
|
|
180
|
+
const pending = this.queue.splice(0);
|
|
181
|
+
for (const item of pending) {
|
|
182
|
+
item.reject(new Error('queue:cleared'));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
179
186
|
async run() {
|
|
180
187
|
if (this.running) {
|
|
181
188
|
return;
|
|
@@ -216,6 +223,7 @@ class Queue {
|
|
|
216
223
|
* ```
|
|
217
224
|
* @returns {{
|
|
218
225
|
* add: <T>(task: () => Promise<T>) => Promise<T>,
|
|
226
|
+
* clear: () => void,
|
|
219
227
|
* count: () => number,
|
|
220
228
|
* hasTasks: () => boolean
|
|
221
229
|
* }}
|
|
@@ -224,6 +232,7 @@ function asyncTaskQueue() {
|
|
|
224
232
|
const queue = new Queue();
|
|
225
233
|
return {
|
|
226
234
|
add: (task) => queue.add(task),
|
|
235
|
+
clear: () => queue.clear(),
|
|
227
236
|
count: () => queue.count(),
|
|
228
237
|
hasTasks: () => queue.hasTasks()
|
|
229
238
|
};
|
|
@@ -129,7 +129,27 @@ module.exports = {
|
|
|
129
129
|
// without the prefix. This is the legacy behavior of
|
|
130
130
|
// `apos.page.getBaseUrl(req)` before the delegation to this
|
|
131
131
|
// method.
|
|
132
|
-
|
|
132
|
+
//
|
|
133
|
+
// ### `options.relative`
|
|
134
|
+
//
|
|
135
|
+
// When `true`, the returned URL is relative and prefix-qualified.
|
|
136
|
+
// The `prefix` option and i18n hosts are ignored in this case.
|
|
137
|
+
//
|
|
138
|
+
// ### `options.localePrefix`
|
|
139
|
+
//
|
|
140
|
+
// When `true`, the locale prefix (e.g. `/fr`) is appended
|
|
141
|
+
// after the global prefix. Defaults to `false` for
|
|
142
|
+
// backward compatibility.
|
|
143
|
+
getBaseUrl(req, {
|
|
144
|
+
strict = false, prefix = true, relative = false,
|
|
145
|
+
localePrefix = false
|
|
146
|
+
} = {}) {
|
|
147
|
+
if (relative) {
|
|
148
|
+
const result = self.apos.prefix || '';
|
|
149
|
+
return localePrefix
|
|
150
|
+
? result + (self.apos.i18n.locales[req.locale]?.prefix || '')
|
|
151
|
+
: result;
|
|
152
|
+
}
|
|
133
153
|
const hostname = self.apos.i18n.locales?.[req.locale]?.hostname;
|
|
134
154
|
if (hostname) {
|
|
135
155
|
// Locale hostnames are fully qualified origins;
|
|
@@ -137,13 +157,16 @@ module.exports = {
|
|
|
137
157
|
return `${req.protocol}://${hostname}`;
|
|
138
158
|
}
|
|
139
159
|
const aposPrefix = prefix ? (self.apos.prefix || '') : '';
|
|
160
|
+
const lPrefix = localePrefix
|
|
161
|
+
? (self.apos.i18n.locales[req.locale]?.prefix || '')
|
|
162
|
+
: '';
|
|
140
163
|
if (self.isStaticBuild(req)) {
|
|
141
164
|
const staticUrl = req.staticBaseUrl || '';
|
|
142
165
|
if (staticUrl || !strict) {
|
|
143
|
-
return staticUrl + aposPrefix;
|
|
166
|
+
return staticUrl + aposPrefix + lPrefix;
|
|
144
167
|
}
|
|
145
168
|
}
|
|
146
|
-
return (self.apos.baseUrl || '') + aposPrefix;
|
|
169
|
+
return (self.apos.baseUrl || '') + aposPrefix + lPrefix;
|
|
147
170
|
},
|
|
148
171
|
|
|
149
172
|
// Build filter URLs. `data` is an object whose properties
|
|
@@ -362,7 +385,7 @@ module.exports = {
|
|
|
362
385
|
// attachments: { // null when not requested
|
|
363
386
|
// uploadsUrl: '/uploads',
|
|
364
387
|
// results: [
|
|
365
|
-
// { _id: 'abc', urls: [{ size?, path }] },
|
|
388
|
+
// { _id: 'abc', urls: [{ size?, path }], base? },
|
|
366
389
|
// ...
|
|
367
390
|
// ]
|
|
368
391
|
// }
|
|
@@ -502,6 +525,14 @@ module.exports = {
|
|
|
502
525
|
// - `_id` (string): the attachment record ID.
|
|
503
526
|
// - `urls` (array): `{ size, path }` objects where `path`
|
|
504
527
|
// is the uploadfs-relative file path.
|
|
528
|
+
// - `base` (string, optional): when present, overrides the
|
|
529
|
+
// global `uploadsUrl` for this entry. Set by
|
|
530
|
+
// `@apostrophecms/file.applyPrettyUrlPaths` for file
|
|
531
|
+
// pieces with pretty URLs enabled. The value is a
|
|
532
|
+
// relative, prefix-qualified path (e.g. `/files` or
|
|
533
|
+
// `/cms/files`). Consumers should use
|
|
534
|
+
// `entry.base || attachments.uploadsUrl` as the download
|
|
535
|
+
// and output base for each entry.
|
|
505
536
|
//
|
|
506
537
|
// After attachment metadata is collected, the
|
|
507
538
|
// `@apostrophecms/url:getAllAttachmentMetadata` event is
|
|
@@ -563,6 +594,9 @@ module.exports = {
|
|
|
563
594
|
skipSizes
|
|
564
595
|
})
|
|
565
596
|
};
|
|
597
|
+
await self.apos.file.applyPrettyUrlPaths(
|
|
598
|
+
req, response.attachments
|
|
599
|
+
);
|
|
566
600
|
await self.emit(
|
|
567
601
|
'getAllAttachmentMetadata',
|
|
568
602
|
req,
|