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.
Files changed (67) hide show
  1. package/.eslintignore +1 -0
  2. package/.stylelintignore +4 -0
  3. package/CHANGELOG.md +43 -0
  4. package/lib/universal/check-if-conditions.mjs +209 -0
  5. package/modules/@apostrophecms/admin-bar/index.js +1 -1
  6. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +5 -2
  7. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBreakpointPreviewMode.vue +77 -30
  8. package/modules/@apostrophecms/area/index.js +61 -1
  9. package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +169 -8
  10. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +15 -12
  11. package/modules/@apostrophecms/area/ui/apos/components/AposWidgetControls.vue +1 -1
  12. package/modules/@apostrophecms/doc-type/index.js +23 -6
  13. package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +1 -1
  14. package/modules/@apostrophecms/express/index.js +23 -1
  15. package/modules/@apostrophecms/i18n/i18n/de.json +26 -6
  16. package/modules/@apostrophecms/i18n/i18n/en.json +27 -5
  17. package/modules/@apostrophecms/i18n/i18n/es.json +24 -4
  18. package/modules/@apostrophecms/i18n/i18n/fr.json +24 -4
  19. package/modules/@apostrophecms/i18n/i18n/it.json +28 -8
  20. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +24 -4
  21. package/modules/@apostrophecms/i18n/i18n/sk.json +23 -3
  22. package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +10 -2
  23. package/modules/@apostrophecms/image/index.js +72 -0
  24. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +98 -17
  25. package/modules/@apostrophecms/image-widget/index.js +130 -4
  26. package/modules/@apostrophecms/image-widget/views/fragment.html +89 -0
  27. package/modules/@apostrophecms/image-widget/views/widget.html +7 -34
  28. package/modules/@apostrophecms/login/index.js +22 -1
  29. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +32 -5
  30. package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +14 -4
  31. package/modules/@apostrophecms/module/index.js +5 -0
  32. package/modules/@apostrophecms/notification/index.js +1 -1
  33. package/modules/@apostrophecms/page/index.js +30 -9
  34. package/modules/@apostrophecms/piece-type/index.js +16 -4
  35. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +5 -0
  36. package/modules/@apostrophecms/rich-text-widget/index.js +87 -10
  37. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue +164 -12
  38. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +25 -5
  39. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapDivider.vue +3 -1
  40. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +42 -3
  41. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Image.js +220 -17
  42. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Link.js +5 -0
  43. package/modules/@apostrophecms/schema/index.js +150 -82
  44. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +1 -0
  45. package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +43 -67
  46. package/modules/@apostrophecms/schema/ui/apos/lib/conditionalFields.js +51 -57
  47. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputArray.js +34 -7
  48. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputObject.js +19 -1
  49. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputSlug.js +17 -2
  50. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputString.js +20 -2
  51. package/modules/@apostrophecms/schema/ui/apos/mixins/AposInputConditionalFieldsMixin.js +0 -1
  52. package/modules/@apostrophecms/schema/ui/apos/mixins/AposInputFollowingMixin.js +6 -1
  53. package/modules/@apostrophecms/submitted-draft/index.js +2 -2
  54. package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +5 -0
  55. package/modules/@apostrophecms/ui/ui/apos/components/AposCheckbox.vue +29 -5
  56. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +29 -10
  57. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuTip.vue +2 -2
  58. package/modules/@apostrophecms/ui/ui/apos/components/AposTagApply.vue +494 -323
  59. package/modules/@apostrophecms/ui/ui/apos/scss/global/_breakpoint_preview.scss +9 -2
  60. package/modules/@apostrophecms/ui/ui/apos/scss/global/_inputs.scss +75 -3
  61. package/modules/@apostrophecms/ui/ui/apos/stores/notification.js +1 -6
  62. package/package.json +20 -59
  63. package/test/express.js +136 -1
  64. package/test/filters.js +300 -0
  65. package/test/login.js +80 -0
  66. package/test/schema-conditions.js +1107 -0
  67. package/lib/check-if-conditions.js +0 -62
package/.eslintignore CHANGED
@@ -3,3 +3,4 @@
3
3
  **/node_modules
4
4
  test/public
5
5
  test/apos-build
6
+ coverage
@@ -0,0 +1,4 @@
1
+ **/node_modules
2
+ test/public
3
+ test/apos-build
4
+ coverage
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
+ }
@@ -116,7 +116,7 @@ module.exports = {
116
116
  action: {
117
117
  type: 'command-menu-admin-bar-toggle-publish-draft'
118
118
  },
119
- shortcut: 'Ctrl+Shift+D Meta+Shift+D'
119
+ shortcut: 'Ctrl+Shift+M Meta+Shift+M'
120
120
  },
121
121
  ...breakpointPreviewModeCommands
122
122
  },
@@ -551,7 +551,9 @@ export default {
551
551
  }
552
552
  },
553
553
  async refresh(options = {}) {
554
- const refreshable = document.querySelector('[data-apos-refreshable]');
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]')).filter(el => el.getAttribute('data-doc-id') === this.context._id);
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);
@@ -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('body').setAttribute('data-breakpoint-preview-mode', mode);
161
- document.querySelector('[data-apos-refreshable]').setAttribute('data-resizable', this.resizable);
162
- document.querySelector('[data-apos-refreshable]').setAttribute('data-label', this.$t(label));
163
- document.querySelector('[data-apos-refreshable]').style.width = width;
164
- document.querySelector('[data-apos-refreshable]').style.height = height;
165
- document.querySelector('[data-apos-refreshable]').style.background = this.originalBodyBackground;
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
- label,
184
- width,
185
- height
186
- }) {
187
- if (this.mode === mode || mode === null) {
188
- document.querySelector('body').removeAttribute('data-breakpoint-preview-mode');
189
- document.querySelector('[data-apos-refreshable]').removeAttribute('data-resizable');
190
- document.querySelector('[data-apos-refreshable]').removeAttribute('data-label');
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.switchBreakpointPreviewMode({
203
- mode,
204
- label,
205
- width,
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 = _.find(schema, 'name', name);
607
+ const field = schema?.find(field => field.name === name);
548
608
  if (!(field && field.options)) {
549
609
  return {};
550
610
  }