apostrophe 4.17.1 → 4.19.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/.eslintignore +1 -0
- package/.stylelintignore +4 -0
- package/CHANGELOG.md +43 -0
- package/lib/universal/check-if-conditions.mjs +209 -0
- package/modules/@apostrophecms/admin-bar/index.js +1 -1
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +5 -2
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBreakpointPreviewMode.vue +77 -30
- package/modules/@apostrophecms/area/index.js +61 -1
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +169 -8
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +15 -12
- package/modules/@apostrophecms/area/ui/apos/components/AposWidgetControls.vue +1 -1
- package/modules/@apostrophecms/doc-type/index.js +23 -6
- package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +1 -1
- package/modules/@apostrophecms/express/index.js +23 -1
- package/modules/@apostrophecms/i18n/i18n/de.json +26 -6
- package/modules/@apostrophecms/i18n/i18n/en.json +27 -5
- package/modules/@apostrophecms/i18n/i18n/es.json +24 -4
- package/modules/@apostrophecms/i18n/i18n/fr.json +24 -4
- package/modules/@apostrophecms/i18n/i18n/it.json +28 -8
- package/modules/@apostrophecms/i18n/i18n/pt-BR.json +24 -4
- package/modules/@apostrophecms/i18n/i18n/sk.json +23 -3
- package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +10 -2
- package/modules/@apostrophecms/image/index.js +72 -0
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +98 -17
- package/modules/@apostrophecms/image-widget/index.js +130 -4
- package/modules/@apostrophecms/image-widget/views/fragment.html +89 -0
- package/modules/@apostrophecms/image-widget/views/widget.html +7 -34
- package/modules/@apostrophecms/login/index.js +22 -1
- package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +32 -5
- package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +14 -4
- package/modules/@apostrophecms/module/index.js +5 -0
- package/modules/@apostrophecms/notification/index.js +1 -1
- package/modules/@apostrophecms/page/index.js +30 -9
- package/modules/@apostrophecms/piece-type/index.js +16 -4
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +5 -0
- package/modules/@apostrophecms/rich-text-widget/index.js +87 -10
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue +164 -12
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +25 -5
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapDivider.vue +3 -1
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +42 -3
- package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Image.js +220 -17
- package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Link.js +5 -0
- package/modules/@apostrophecms/schema/index.js +150 -82
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +1 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +43 -67
- package/modules/@apostrophecms/schema/ui/apos/lib/conditionalFields.js +51 -57
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputArray.js +34 -7
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputObject.js +19 -1
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputSlug.js +17 -2
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputString.js +20 -2
- package/modules/@apostrophecms/schema/ui/apos/mixins/AposInputConditionalFieldsMixin.js +0 -1
- package/modules/@apostrophecms/schema/ui/apos/mixins/AposInputFollowingMixin.js +6 -1
- package/modules/@apostrophecms/submitted-draft/index.js +2 -2
- package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +5 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposCheckbox.vue +29 -5
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +29 -10
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuTip.vue +2 -2
- package/modules/@apostrophecms/ui/ui/apos/components/AposTagApply.vue +494 -323
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_breakpoint_preview.scss +9 -2
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_inputs.scss +75 -3
- package/modules/@apostrophecms/ui/ui/apos/stores/notification.js +1 -6
- package/package.json +20 -59
- package/test/express.js +136 -1
- package/test/filters.js +300 -0
- package/test/login.js +80 -0
- package/test/schema-conditions.js +1107 -0
- package/lib/check-if-conditions.js +0 -62
package/.eslintignore
CHANGED
package/.stylelintignore
ADDED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.19.0 (2025-07-09)
|
|
4
|
+
|
|
5
|
+
### Adds
|
|
6
|
+
|
|
7
|
+
* Implemented GET /api/v1/@apostrophecms/login/whoami route such that it returns the details of the currently logged in user; added the route to the login module.
|
|
8
|
+
Thanks to [sombitganguly](https://github.com/sombitganguly) for this contribution.
|
|
9
|
+
* Adds keyboard shortcuts for manipulating widgets in areas. Includes Cut, Copy, Paste, Delete, and Duplicate.
|
|
10
|
+
* Automatic translation now supports a disclaimer and an help text for the checkbox. You can now set the disclaimer by setting `automaticTranslationDisclaimer` `i18n` key and the help text by setting `automaticTranslationCheckboxHelp` `i18n` key.
|
|
11
|
+
* Adds dynamic choices working with piece manager filters.
|
|
12
|
+
* Allow `import.imageTags` (array of image tag IDs) to be passed to the rich text widget when importing (see https://docs.apostrophecms.org/reference/api/rich-text.html#importing-inline-images).
|
|
13
|
+
* Adds a new way to make `GET` requests with a large query string. It can become a `POST` request containing the key `__aposGetWithQuery` in its body.
|
|
14
|
+
A middleware checks for this key and converts the request back to a `GET` request with the right `req.query` property.
|
|
15
|
+
* Adds a new batch operation to tag images.
|
|
16
|
+
|
|
17
|
+
### Changes
|
|
18
|
+
|
|
19
|
+
### Fixes
|
|
20
|
+
|
|
21
|
+
* Add missing Pages manager shortcuts list helper.
|
|
22
|
+
* Improve the `isEmpty` method of the rich text widget to take into account the HTML blocks (`<figure>` and `<table>`) that are not empty but do not contain any plain text.
|
|
23
|
+
* (Backward compatibility break) Conditional field that depends on already hidden field is also hidden, again.
|
|
24
|
+
|
|
25
|
+
## 4.18.0 (2025-06-11)
|
|
26
|
+
|
|
27
|
+
### Adds
|
|
28
|
+
|
|
29
|
+
* Adds MongoDB-style support (comparison operators) for conditional fields and all systems that use conditions. Conditional fields now have access to the `following` values from the parent schema fields.
|
|
30
|
+
* Add `followingIgnore` option to the `string` field schema. A boolean `true` results in all `following` values being ignored (not attempted to be used as a value for the field). When array of strings, the UI will ignore every item that matches a `following` field name.
|
|
31
|
+
* Adds link configuration to the `@apostrophecms/image-widget` UI and a new option `linkWithType` to control what document types can be linked to. Opt-out of the widget inline styles (reset) by setting `inlineStyles: false` in the widget configuration or contextual options (area).
|
|
32
|
+
* Use the link configuration of the Rich Text widget for image links too. It respects the existing `linkWithType` Rich Text option and uses the same schema (`linkFields`) used for text links. The fields from that schema can opt-in for specific tiptap extension now via a field property `extensions` (array) with possible array values `Link` and/or `Image`. You still need to specify the `htmlAttribute` property (the name of the attribute to be added to the link tag) in the schema when adding more fields. If the `extensions` property is not set, the field will be applied for both tiptap extensions.
|
|
33
|
+
* Adds body style support for breakpoint preview mode. Created new `[data-apos-refreshable-body]` div inside the container during breapoint preview.
|
|
34
|
+
Switch body attributes to this new div to keep supporting body styles in breakpoint preview mode.
|
|
35
|
+
|
|
36
|
+
### Changes
|
|
37
|
+
|
|
38
|
+
* Set the `Cache-Control` header to `no-store` for error pages in order to prevent the risk of serving stale error pages to users.
|
|
39
|
+
* Updates rich-text default configuration.
|
|
40
|
+
|
|
41
|
+
### Fixes
|
|
42
|
+
|
|
43
|
+
* The Download links in the media library now immediately download the file as expected, rather than navigating to the image in the current tab. `AposButton` now supports the `:download="true"` prop as expected.
|
|
44
|
+
* Using an API key with the editor, contributor or guest role now have a `req` object with the corresponding rights. The old behavior gave non-admin API keys less access than expected.
|
|
45
|
+
|
|
3
46
|
## 4.17.1 (2025-05-16)
|
|
4
47
|
|
|
5
48
|
### Fixes
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
|
|
2
|
+
// NOTE: This is a universal library for evaluating MongoDB-style conditions
|
|
3
|
+
// against a document or sub-document. Do not use any browser or node specific
|
|
4
|
+
// APIs here.
|
|
5
|
+
//
|
|
6
|
+
//
|
|
7
|
+
// Evaluate a field conditions against current schema values (doc or sub-doc).
|
|
8
|
+
// Expects a `conditions` object containing MongoDB-style conditions.
|
|
9
|
+
// Operators `$or` and `$and` are supported as top-level keys to allow
|
|
10
|
+
// for complex logical conditions. All comparison MongoDB operators are supported
|
|
11
|
+
// as object values.
|
|
12
|
+
// The condition object properties are field paths (dot notation is supported
|
|
13
|
+
// for objects), and the values are the conditions to evaluate against the
|
|
14
|
+
// document values.
|
|
15
|
+
// The `voterFn` function can be provided for evaluating
|
|
16
|
+
// conditions - return `false` to stop further evaluation.
|
|
17
|
+
// The function is called with three arguments:
|
|
18
|
+
// `voterFn(propName, condition, docValue)` where `propName` is the condition field path,
|
|
19
|
+
// `condition` is the condition value, and `docValue` is the field value extracted
|
|
20
|
+
// from the document.
|
|
21
|
+
// Example usage:
|
|
22
|
+
// ```js
|
|
23
|
+
// doc = {
|
|
24
|
+
// assignee: 'john',
|
|
25
|
+
// status: 'active',
|
|
26
|
+
// stats: { count: 15 },
|
|
27
|
+
// category: 'news'
|
|
28
|
+
// };
|
|
29
|
+
// const conditions = {
|
|
30
|
+
// assignee: { '$exists': true },
|
|
31
|
+
// status: { '$in': ['active', 'pending'] },
|
|
32
|
+
// 'stats.count': { '$gte': 10 },
|
|
33
|
+
// $or: [
|
|
34
|
+
// { 'category': 'news' },
|
|
35
|
+
// { 'category': 'blog' }
|
|
36
|
+
// ]
|
|
37
|
+
// };
|
|
38
|
+
// function voterFn(propName, condition, docValue) {
|
|
39
|
+
// if (propName === 'status' && docValue === 'inactive') {
|
|
40
|
+
// return false; // Reject the condition for 'status' if it's 'inactive'
|
|
41
|
+
// }
|
|
42
|
+
// // Non-boolean return values are ignored
|
|
43
|
+
// }
|
|
44
|
+
// const result = checkIfConditions(doc, conditions, voterFn);
|
|
45
|
+
// ```
|
|
46
|
+
export default function checkIfConditions(doc, conditions, voterFn = () => null) {
|
|
47
|
+
return Object.entries(conditions).every(([ key, condition ]) => {
|
|
48
|
+
if (key === '$or') {
|
|
49
|
+
return checkOrConditions(doc, condition, voterFn);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (key === '$and') {
|
|
53
|
+
return checkAndConditions(doc, condition, voterFn);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const docValue = getNestedPropValue(doc, key);
|
|
57
|
+
|
|
58
|
+
// If the custom voter rejects the condition, we stop evaluating
|
|
59
|
+
// and return false immediately.
|
|
60
|
+
if (voterFn(key, condition, docValue) === false) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// External conditions should be handled outside of this function,
|
|
65
|
+
// so we skip them here. Use the `voterFn` to gather external condition
|
|
66
|
+
// entries and evaluate them separately.
|
|
67
|
+
if (isExternalCondition(key)) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Otherwise, we evaluate the condition against the document value.
|
|
72
|
+
return evaluate(docValue, condition);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function isExternalCondition(conditionKey) {
|
|
77
|
+
return conditionKey.endsWith(')');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function checkOrConditions(doc, conditions, voterFn) {
|
|
81
|
+
return conditions.some((condition) => {
|
|
82
|
+
return checkIfConditions(doc, condition, voterFn);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function checkAndConditions(doc, conditions, voterFn) {
|
|
87
|
+
return conditions.every((condition) => {
|
|
88
|
+
return checkIfConditions(doc, condition, voterFn);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getNestedPropValue(doc, key) {
|
|
93
|
+
if (!key.includes('.')) {
|
|
94
|
+
return doc?.[key];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const keys = key.split('.');
|
|
98
|
+
let currentValue = doc;
|
|
99
|
+
while (keys.length > 0) {
|
|
100
|
+
if (
|
|
101
|
+
// The `==` comparison is intentionally used here to match
|
|
102
|
+
// both `null` and `undefined`
|
|
103
|
+
currentValue == null ||
|
|
104
|
+
// Support i.e. `stringField.length`
|
|
105
|
+
// eslint-disable-next-line valid-typeof
|
|
106
|
+
typeof currentValue?.[keys[0]] == null
|
|
107
|
+
) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
if (Array.isArray(currentValue)) {
|
|
111
|
+
// Support i.e. `arrayField.length` or `arrayField.0`
|
|
112
|
+
if (!Object.hasOwn(currentValue, keys[0])) {
|
|
113
|
+
return currentValue.flatMap(item => {
|
|
114
|
+
return getNestedPropValue(item, keys.join('.'));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const prop = keys.shift();
|
|
119
|
+
currentValue = currentValue[prop];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return currentValue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Comparison operators registry for MongoDB-style conditions.
|
|
126
|
+
// https://www.mongodb.com/docs/manual/reference/operator/query-comparison/
|
|
127
|
+
const opRegistry = {};
|
|
128
|
+
opRegistry.$eq = (docValue, conditionValue) => {
|
|
129
|
+
if (Array.isArray(docValue)) {
|
|
130
|
+
if (Array.isArray(conditionValue)) {
|
|
131
|
+
// Unlike MongoDB, we don't match the index order of the arrays.
|
|
132
|
+
return docValue.length === conditionValue.length &&
|
|
133
|
+
conditionValue.every(value => docValue.includes(value));
|
|
134
|
+
}
|
|
135
|
+
return docValue.includes(conditionValue);
|
|
136
|
+
}
|
|
137
|
+
return docValue === conditionValue;
|
|
138
|
+
};
|
|
139
|
+
opRegistry.$ne = (docValue, conditionValue) => (
|
|
140
|
+
opRegistry.$eq(docValue, conditionValue) === false
|
|
141
|
+
);
|
|
142
|
+
opRegistry.$exists = (docValue, conditionValue) => {
|
|
143
|
+
// Per MongoDB documentation, $exists should treat null and undefined the same.
|
|
144
|
+
// == null and != null are documented to match or reject null, undefined
|
|
145
|
+
// and nothing else.
|
|
146
|
+
return (conditionValue ? docValue != null : docValue == null);
|
|
147
|
+
};
|
|
148
|
+
opRegistry.$in = (docValue, conditionValue) => {
|
|
149
|
+
if (!Array.isArray(conditionValue)) {
|
|
150
|
+
throw new Error('$in and $nin operators require an array as condition value');
|
|
151
|
+
}
|
|
152
|
+
if (Array.isArray(docValue)) {
|
|
153
|
+
return conditionValue.some(value => docValue.includes(value));
|
|
154
|
+
}
|
|
155
|
+
return conditionValue.includes(docValue);
|
|
156
|
+
};
|
|
157
|
+
opRegistry.$nin = (docValue, conditionValue) => (
|
|
158
|
+
opRegistry.$in(docValue, conditionValue) === false
|
|
159
|
+
);
|
|
160
|
+
opRegistry.$gt = (docValue, conditionValue) => (
|
|
161
|
+
docValue > conditionValue
|
|
162
|
+
);
|
|
163
|
+
opRegistry.$lt = (docValue, conditionValue) => (
|
|
164
|
+
docValue < conditionValue
|
|
165
|
+
);
|
|
166
|
+
opRegistry.$gte = (docValue, conditionValue) => (
|
|
167
|
+
docValue >= conditionValue
|
|
168
|
+
);
|
|
169
|
+
opRegistry.$lte = (docValue, conditionValue) => (
|
|
170
|
+
docValue <= conditionValue
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
export function evaluate(docValue, conditionValue) {
|
|
174
|
+
if (
|
|
175
|
+
typeof conditionValue === 'object' &&
|
|
176
|
+
conditionValue !== null
|
|
177
|
+
) {
|
|
178
|
+
// Empty objects are not valid conditions
|
|
179
|
+
if (Object.keys(conditionValue).length === 0) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
// Evaluate every operator in the conditionValue object.
|
|
183
|
+
// A MongoDB-style condition object is expected.
|
|
184
|
+
// We check for the presence of known comparison operators and
|
|
185
|
+
// evaluate them accordingly.
|
|
186
|
+
return Object.entries(conditionValue).every(([ operator, operand ]) => {
|
|
187
|
+
switch (operator) {
|
|
188
|
+
// BC, support the min and max properties because they were already supported
|
|
189
|
+
// in a different routine.
|
|
190
|
+
case 'min':
|
|
191
|
+
return docValue >= operand;
|
|
192
|
+
case 'max':
|
|
193
|
+
return docValue <= operand;
|
|
194
|
+
default: {
|
|
195
|
+
if (opRegistry[operator]) {
|
|
196
|
+
return opRegistry[operator](docValue, operand);
|
|
197
|
+
}
|
|
198
|
+
throw new Error(`Unsupported operator: ${operator}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (Array.isArray(docValue)) {
|
|
205
|
+
return docValue.includes(conditionValue);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return conditionValue === docValue;
|
|
209
|
+
}
|
|
@@ -551,7 +551,9 @@ export default {
|
|
|
551
551
|
}
|
|
552
552
|
},
|
|
553
553
|
async refresh(options = {}) {
|
|
554
|
-
|
|
554
|
+
// In breakpoint preview mode, uses the fake body.
|
|
555
|
+
const refreshable = document.querySelector('[data-apos-refreshable-body]') ||
|
|
556
|
+
document.querySelector('[data-apos-refreshable]');
|
|
555
557
|
if (options.scrollcheck) {
|
|
556
558
|
window.apos.adminBar.scrollPosition = {
|
|
557
559
|
x: window.scrollX,
|
|
@@ -613,7 +615,8 @@ export default {
|
|
|
613
615
|
// "@ notation" PATCH feature. Sort the areas by DOM depth
|
|
614
616
|
// to ensure parents patch before children
|
|
615
617
|
this.original = {};
|
|
616
|
-
const els = Array.from(document.querySelectorAll('[data-apos-area-newly-editable]'))
|
|
618
|
+
const els = Array.from(document.querySelectorAll('[data-apos-area-newly-editable]'))
|
|
619
|
+
.filter(el => el.getAttribute('data-doc-id') === this.context._id);
|
|
617
620
|
els.sort((a, b) => {
|
|
618
621
|
const da = depth(a);
|
|
619
622
|
const db = depth(b);
|
package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBreakpointPreviewMode.vue
CHANGED
|
@@ -56,7 +56,6 @@
|
|
|
56
56
|
</div>
|
|
57
57
|
</template>
|
|
58
58
|
<script>
|
|
59
|
-
|
|
60
59
|
export default {
|
|
61
60
|
name: 'TheAposContextBreakpointPreviewMode',
|
|
62
61
|
props: {
|
|
@@ -88,7 +87,10 @@ export default {
|
|
|
88
87
|
originalBodyBackground: null,
|
|
89
88
|
shortcuts: this.getShortcuts(),
|
|
90
89
|
breakpoints: this.getBreakpointItems(),
|
|
91
|
-
showDropdown: false
|
|
90
|
+
showDropdown: false,
|
|
91
|
+
bodyEl: null,
|
|
92
|
+
refreshableBodyEl: null,
|
|
93
|
+
observer: new MutationObserver(this.observerCallback)
|
|
92
94
|
};
|
|
93
95
|
},
|
|
94
96
|
computed: {
|
|
@@ -130,6 +132,7 @@ export default {
|
|
|
130
132
|
}
|
|
131
133
|
},
|
|
132
134
|
mounted() {
|
|
135
|
+
this.bodyEl = document.querySelector('body');
|
|
133
136
|
this.setShowDropdown();
|
|
134
137
|
apos.bus.$on(
|
|
135
138
|
'command-menu-admin-bar-toggle-breakpoint-preview-mode',
|
|
@@ -151,18 +154,56 @@ export default {
|
|
|
151
154
|
);
|
|
152
155
|
},
|
|
153
156
|
methods: {
|
|
157
|
+
observerCallback(mutationList, observer) {
|
|
158
|
+
for (const mutation of mutationList) {
|
|
159
|
+
if (
|
|
160
|
+
mutation.type !== 'attributes' ||
|
|
161
|
+
mutation.attributeName.startsWith('data-apos') ||
|
|
162
|
+
mutation.attributeName === 'data-breakpoint-preview-mode'
|
|
163
|
+
) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const bodyAttribute = mutation.target
|
|
167
|
+
.getAttribute(mutation.attributeName);
|
|
168
|
+
this.refreshableBodyEl.setAttribute(mutation.attributeName, bodyAttribute);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
createFakeBody(refreshableEl) {
|
|
173
|
+
this.refreshableBodyEl = document.createElement('div');
|
|
174
|
+
this.refreshableBodyEl.setAttribute('data-apos-refreshable-body', '');
|
|
175
|
+
Array.from(refreshableEl.childNodes).forEach(child => {
|
|
176
|
+
this.refreshableBodyEl.append(child);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
Array.from(this.bodyEl.attributes || {})
|
|
180
|
+
.filter(({ name }) => !name.startsWith('data-apos'))
|
|
181
|
+
.forEach(({ name, value }) => {
|
|
182
|
+
this.refreshableBodyEl.setAttribute(name, value);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
refreshableEl.append(this.refreshableBodyEl);
|
|
186
|
+
},
|
|
187
|
+
|
|
154
188
|
switchBreakpointPreviewMode({
|
|
155
189
|
mode,
|
|
156
190
|
label,
|
|
157
191
|
width,
|
|
158
192
|
height
|
|
159
193
|
}) {
|
|
160
|
-
document.querySelector('
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
194
|
+
const refreshableEl = document.querySelector('[data-apos-refreshable]');
|
|
195
|
+
|
|
196
|
+
// Only when switching to mobile preview from the normal state
|
|
197
|
+
if (!this.mode) {
|
|
198
|
+
this.createFakeBody(refreshableEl);
|
|
199
|
+
this.observer.observe(this.bodyEl, { attributes: true });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this.bodyEl.setAttribute('data-breakpoint-preview-mode', mode);
|
|
203
|
+
refreshableEl.setAttribute('data-resizable', this.resizable);
|
|
204
|
+
refreshableEl.setAttribute('data-label', this.$t(label));
|
|
205
|
+
refreshableEl.style.width = width;
|
|
206
|
+
refreshableEl.style.height = height;
|
|
166
207
|
|
|
167
208
|
this.mode = mode;
|
|
168
209
|
this.$emit('switch-breakpoint-preview-mode', {
|
|
@@ -178,33 +219,39 @@ export default {
|
|
|
178
219
|
height
|
|
179
220
|
});
|
|
180
221
|
},
|
|
181
|
-
toggleBreakpointPreviewMode({
|
|
182
|
-
mode
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
document.querySelector('[data-apos-refreshable]').style.removeProperty('width');
|
|
192
|
-
document.querySelector('[data-apos-refreshable]').style.removeProperty('height');
|
|
193
|
-
document.querySelector('[data-apos-refreshable]').style.removeProperty('background');
|
|
194
|
-
|
|
195
|
-
this.mode = null;
|
|
196
|
-
this.$emit('reset-breakpoint-preview-mode');
|
|
197
|
-
this.saveState({ mode: this.mode });
|
|
222
|
+
toggleBreakpointPreviewMode(state) {
|
|
223
|
+
if (this.mode === state.mode || state.mode === null) {
|
|
224
|
+
this.resetBreakpointPreview();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
this.switchBreakpointPreviewMode(state);
|
|
229
|
+
},
|
|
230
|
+
resetBreakpointPreview() {
|
|
231
|
+
const refreshableEl = document.querySelector('[data-apos-refreshable]');
|
|
198
232
|
|
|
233
|
+
this.observer.disconnect();
|
|
234
|
+
if (!this.refreshableBodyEl) {
|
|
199
235
|
return;
|
|
200
236
|
}
|
|
201
237
|
|
|
202
|
-
this.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
height
|
|
238
|
+
Array.from(this.refreshableBodyEl.childNodes).forEach(child => {
|
|
239
|
+
if (child.nodeType !== Node.TEXT_NODE || child.nodeValue.trim()) {
|
|
240
|
+
refreshableEl.append(child);
|
|
241
|
+
}
|
|
207
242
|
});
|
|
243
|
+
this.refreshableBodyEl.remove();
|
|
244
|
+
this.refreshableBodyEl = null;
|
|
245
|
+
|
|
246
|
+
this.bodyEl.removeAttribute('data-breakpoint-preview-mode');
|
|
247
|
+
refreshableEl.removeAttribute('data-resizable');
|
|
248
|
+
refreshableEl.removeAttribute('data-label');
|
|
249
|
+
refreshableEl.style.removeProperty('width');
|
|
250
|
+
refreshableEl.style.removeProperty('height');
|
|
251
|
+
|
|
252
|
+
this.mode = null;
|
|
253
|
+
this.$emit('reset-breakpoint-preview-mode');
|
|
254
|
+
this.saveState({ mode: this.mode });
|
|
208
255
|
},
|
|
209
256
|
loadState() {
|
|
210
257
|
return JSON.parse(sessionStorage.getItem('aposBreakpointPreviewMode') || '{}');
|
|
@@ -9,6 +9,66 @@ const { stripIndent } = require('common-tags');
|
|
|
9
9
|
|
|
10
10
|
module.exports = {
|
|
11
11
|
options: { alias: 'area' },
|
|
12
|
+
commands(self) {
|
|
13
|
+
return {
|
|
14
|
+
add: {
|
|
15
|
+
[`${self.__meta.name}:cut-widget`]: {
|
|
16
|
+
type: 'item',
|
|
17
|
+
label: 'apostrophe:commandMenuWidgetCut',
|
|
18
|
+
action: {
|
|
19
|
+
type: 'command-menu-area-cut-widget'
|
|
20
|
+
},
|
|
21
|
+
shortcut: 'Ctrl+X Meta+X'
|
|
22
|
+
},
|
|
23
|
+
[`${self.__meta.name}:copy-widget`]: {
|
|
24
|
+
type: 'item',
|
|
25
|
+
label: 'apostrophe:commandMenuWidgetCopy',
|
|
26
|
+
action: {
|
|
27
|
+
type: 'command-menu-area-copy-widget'
|
|
28
|
+
},
|
|
29
|
+
shortcut: 'Ctrl+C Meta+C'
|
|
30
|
+
},
|
|
31
|
+
[`${self.__meta.name}:paste-widget`]: {
|
|
32
|
+
type: 'item',
|
|
33
|
+
label: 'apostrophe:commandMenuWidgetPaste',
|
|
34
|
+
action: {
|
|
35
|
+
type: 'command-menu-area-paste-widget'
|
|
36
|
+
},
|
|
37
|
+
shortcut: 'Ctrl+V Meta+V'
|
|
38
|
+
},
|
|
39
|
+
[`${self.__meta.name}:duplicate-widget`]: {
|
|
40
|
+
type: 'item',
|
|
41
|
+
label: 'apostrophe:commandMenuWidgetDuplicate',
|
|
42
|
+
action: {
|
|
43
|
+
type: 'command-menu-area-duplicate-widget'
|
|
44
|
+
},
|
|
45
|
+
shortcut: 'Ctrl+Shift+D Meta+Shift+D'
|
|
46
|
+
},
|
|
47
|
+
[`${self.__meta.name}:remove-widget`]: {
|
|
48
|
+
type: 'item',
|
|
49
|
+
label: 'apostrophe:commandMenuWidgetRemove',
|
|
50
|
+
action: {
|
|
51
|
+
type: 'command-menu-area-remove-widget'
|
|
52
|
+
},
|
|
53
|
+
shortcut: 'Backspace'
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
modal: {
|
|
57
|
+
default: {
|
|
58
|
+
'@apostrophecms/command-menu:content': {
|
|
59
|
+
label: 'apostrophe:commandMenuContent',
|
|
60
|
+
commands: [
|
|
61
|
+
`${self.__meta.name}:cut-widget`,
|
|
62
|
+
`${self.__meta.name}:copy-widget`,
|
|
63
|
+
`${self.__meta.name}:paste-widget`,
|
|
64
|
+
`${self.__meta.name}:duplicate-widget`,
|
|
65
|
+
`${self.__meta.name}:remove-widget`
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
},
|
|
12
72
|
init(self) {
|
|
13
73
|
// These properties have special meaning in Apostrophe docs and are not
|
|
14
74
|
// acceptable for use as top-level area names
|
|
@@ -544,7 +604,7 @@ module.exports = {
|
|
|
544
604
|
return {};
|
|
545
605
|
}
|
|
546
606
|
const schema = manager.schema;
|
|
547
|
-
const field =
|
|
607
|
+
const field = schema?.find(field => field.name === name);
|
|
548
608
|
if (!(field && field.options)) {
|
|
549
609
|
return {};
|
|
550
610
|
}
|