apostrophe 3.28.1 → 3.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.
@@ -0,0 +1,264 @@
1
+ <template>
2
+ <AposModal
3
+ class="apos-area-menu--expanded"
4
+ :modal="modal"
5
+ modal-title="apostrophe:addContent"
6
+ @inactive="modal.active = false"
7
+ @show-modal="modal.showModal = true"
8
+ @esc="close"
9
+ @no-modal="$emit('safe-close')"
10
+ >
11
+ <template #main>
12
+ <AposModalBody>
13
+ <template #bodyMain>
14
+ <div
15
+ v-for="(group, groupIndex) in groups"
16
+ :key="groupIndex"
17
+ class="apos-widget-group"
18
+ >
19
+ <h2 class="apos-widget-group__label" v-if="group.label">{{ group.label }}</h2>
20
+ <div
21
+ :class="[
22
+ `apos-widget-group--${group.columns}-column${
23
+ group.columns > 1 ? 's' : ''
24
+ }`
25
+ ]"
26
+ >
27
+ <div
28
+ v-for="(item, itemIndex) in group.widgets"
29
+ :key="itemIndex"
30
+ class="apos-widget"
31
+ @click="add(item)"
32
+ >
33
+ <div class="apos-widget__preview">
34
+ <plus-icon
35
+ :size="20"
36
+ class="apos-icon--add"
37
+ />
38
+ <img
39
+ v-if="item.previewImage"
40
+ :src="previewUrl(item)"
41
+ :alt="`${item.name} preview`"
42
+ class="apos-widget__preview-image"
43
+ >
44
+ <component
45
+ v-else-if="hasIcon(item)"
46
+ :size="25"
47
+ class="apos-widget__preview--icon"
48
+ :is="item.previewIcon || item.icon"
49
+ />
50
+ </div>
51
+ <p class="apos-widget__label">
52
+ {{ $t(item.label) }}
53
+ </p>
54
+ <p v-if="item.description" class="apos-widget__help">
55
+ {{ $t(item.description) }}
56
+ </p>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ </template>
61
+ </AposModalBody>
62
+ </template>
63
+ </AposModal>
64
+ </template>
65
+
66
+ <script>
67
+ export default {
68
+ name: 'AposAreaExpandedMenu',
69
+ props: {
70
+ options: {
71
+ type: Object,
72
+ required: true
73
+ },
74
+ index: {
75
+ type: Number,
76
+ default: 0
77
+ }
78
+ },
79
+ emits: [ 'expanded-menu-close', 'safe-close', 'modal-result' ],
80
+ data() {
81
+ return {
82
+ modal: {
83
+ active: false,
84
+ type: 'slide',
85
+ origin: 'left',
86
+ showModal: false,
87
+ width: 'one-third'
88
+ },
89
+ groups: []
90
+ };
91
+ },
92
+ async mounted() {
93
+ this.modal.active = true;
94
+
95
+ if (this.options.groups) {
96
+ for (const item of Object.keys(this.options.groups)) {
97
+ if (!this.isValidColumn(item.columns)) {
98
+ console.warn(
99
+ `apos.expanded-menu: The specified number of columns for the group ${item.label} is not between the allowed range of 1-4.`
100
+ );
101
+ }
102
+
103
+ const group = this.createGroup(this.options.groups[item]);
104
+ this.groups.push(group);
105
+ }
106
+ } else if (this.options.widgets) {
107
+ if (!this.isValidColumn(this.options.columns)) {
108
+ console.warn(
109
+ 'apos.expanded-menu: The specified number of columns for the area is not between the allowed range of 1-4.'
110
+ );
111
+ }
112
+
113
+ const group = this.createGroup(this.options);
114
+ this.groups.push(group);
115
+ } else {
116
+ console.warn(
117
+ 'apos.expanded-menu: No groups or widgets defined. Please, either add a groups or widgets property to your area configuration.'
118
+ );
119
+ }
120
+ },
121
+ methods: {
122
+ isValidColumn(count) {
123
+ return count ? +count > 1 && +count < 4 : true;
124
+ },
125
+ createGroup(config) {
126
+ const group = {
127
+ columns: +config.columns || 3,
128
+ widgets: []
129
+ };
130
+
131
+ if (config.label) {
132
+ group.label = config.label;
133
+ }
134
+
135
+ for (const item of Object.keys(config.widgets)) {
136
+ group.widgets.push(apos.modules[`${item}-widget`]);
137
+ }
138
+
139
+ return group;
140
+ },
141
+ hasIcon(widget) {
142
+ return widget.previewIcon || widget.icon;
143
+ },
144
+ previewUrl(widget) {
145
+ return widget.previewImage ? apos.util.assetUrl(`/modules/${widget.name}-widget/preview.${widget.previewImage}`) : '';
146
+ },
147
+ close() {
148
+ this.modal.showModal = false;
149
+ },
150
+ async add(item) {
151
+ const data = {
152
+ ...item,
153
+ index: this.index
154
+ };
155
+ this.$emit('modal-result', data);
156
+ this.modal.showModal = false;
157
+ }
158
+ }
159
+ };
160
+ </script>
161
+
162
+ <style lang="scss" scoped>
163
+ .apos-area-menu--expanded {
164
+ @include type-base;
165
+ }
166
+
167
+ .apos-widget-group {
168
+ &:not(:last-of-type) {
169
+ margin-bottom: 30px;
170
+ }
171
+
172
+ .apos-widget__preview {
173
+ position: relative;
174
+ display: flex;
175
+ justify-content: center;
176
+ align-items: center;
177
+ height: 135px;
178
+ border: 1px solid var(--a-base-7);
179
+ border-radius: var(--a-border-radius);
180
+ background-color: var(--a-base-10);
181
+ }
182
+
183
+ &--2-columns {
184
+ display: grid;
185
+ grid-template-columns: repeat(2, 1fr);
186
+ gap: 15px;
187
+ }
188
+
189
+ &--3-columns {
190
+ display: grid;
191
+ grid-template-columns: repeat(3, 1fr);
192
+ gap: 10px;
193
+ .apos-widget__preview {
194
+ height: 89px;
195
+ }
196
+ }
197
+
198
+ &--4-columns {
199
+ display: grid;
200
+ grid-template-columns: repeat(4, 1fr);
201
+ gap: 5px;
202
+ .apos-widget__preview {
203
+ height: 66px;
204
+ }
205
+ }
206
+ }
207
+
208
+ .apos-widget {
209
+ .apos-widget__preview {
210
+ transition: opacity 250ms ease-in-out;
211
+ .apos-icon--add {
212
+ z-index: $z-index-default;
213
+ position: absolute;
214
+ // Center in the parent element
215
+ align-self: center;
216
+ justify-self: center;
217
+ // Center the child content
218
+ display: flex;
219
+ justify-content: center;
220
+ align-items: center;
221
+ width: 27px;
222
+ height: 27px;
223
+ border-radius: 50%;
224
+ background-color: var(--a-primary);
225
+ opacity: 0;
226
+ color: var(--a-white);
227
+ }
228
+ &::after {
229
+ transition: all 250ms ease-in-out;
230
+ position: absolute;
231
+ content: '';
232
+ width: 100%;
233
+ height: 100%;
234
+ background-color: var(--a-primary);
235
+ opacity: 0;
236
+ }
237
+ }
238
+ &:hover {
239
+ cursor: pointer;
240
+ // stylelint-disable max-nesting-depth
241
+ .apos-widget__preview {
242
+ .apos-icon--add {
243
+ opacity: 1;
244
+ }
245
+ &::after {
246
+ opacity: 0.4;
247
+ }
248
+ }
249
+ // stylelint-enable max-nesting-depth
250
+ }
251
+ }
252
+
253
+ .apos-widget__preview-image {
254
+ width: 100%;
255
+ }
256
+
257
+ .apos-widget-group__label,
258
+ .apos-widget__help {
259
+ @include type-base;
260
+ line-height: var(--a-line-tall);
261
+ color: var(--a-base-4);
262
+ text-align: left;
263
+ }
264
+ </style>
@@ -1,91 +1,33 @@
1
1
  <template>
2
- <div class="apos-area-menu" :class="{'apos-is-focused': groupIsFocused}">
3
- <AposContextMenu
4
- :disabled="isDisabled"
5
- :button="buttonOptions"
6
- v-bind="extendedContextMenuOptions"
7
- @open="menuOpen"
8
- @close="menuClose"
9
- ref="contextMenu"
10
- :popover-modifiers="inContext ? ['z-index-in-context'] : []"
11
- >
12
- <ul class="apos-area-menu__wrapper">
13
- <li
14
- class="apos-area-menu__item"
15
- v-for="(item, itemIndex) in myMenu"
16
- :key="item.type ? `${item.type}_${item.label}` : item.label"
17
- :class="{'apos-has-group': item.items}"
18
- :ref="`item-${itemIndex}`"
19
- >
20
- <dl v-if="item.items" class="apos-area-menu__group">
21
- <dt>
22
- <button
23
- :for="item.label" class="apos-area-menu__group-label"
24
- v-if="item.items" tabindex="0"
25
- :id="`${menuId}-trigger-${itemIndex}`"
26
- :aria-controls="`${menuId}-group-${itemIndex}`"
27
- @focus="groupFocused"
28
- @blur="groupBlurred"
29
- @click="toggleGroup(itemIndex)"
30
- @keydown.prevent.space="toggleGroup(itemIndex)"
31
- @keydown.prevent.enter="toggleGroup(itemIndex)"
32
- @keydown.prevent.arrow-down="switchGroup(itemIndex, 1)"
33
- @keydown.prevent.arrow-up="switchGroup(itemIndex, -1)"
34
- @keydown.prevent.home="switchGroup(itemIndex, 0)"
35
- @keydown.prevent.end="switchGroup(itemIndex, null)"
36
- ref="groupButton"
37
- >
38
- <span>{{ item.label }}</span>
39
- <chevron-up-icon
40
- class="apos-area-menu__group-chevron"
41
- :class="{'apos-is-active': itemIndex === active}" :size="13"
42
- />
43
- </button>
44
- </dt>
45
- <dd class="apos-area-menu__group-list" role="region">
46
- <ul
47
- class="apos-area-menu__items apos-area-menu__items--accordion"
48
- :class="{'apos-is-active': active === itemIndex}"
49
- :id="`${menuId}-group-${itemIndex}`"
50
- :aria-labelledby="`${menuId}-trigger-${itemIndex}`"
51
- :aria-expanded="active === itemIndex ? 'true' : 'false'"
52
- >
53
- <li
54
- class="apos-area-menu__item"
55
- v-for="(child, childIndex) in item.items"
56
- :key="child.name"
57
- :ref="`child-${index}-${childIndex}`"
58
- >
59
- <AposAreaMenuItem
60
- @click="add(child)"
61
- :item="child"
62
- :tabbable="itemIndex === active"
63
- @up="switchItem(`child-${itemIndex}-${childIndex - 1}`, -1)"
64
- @down="switchItem(`child-${itemIndex}-${childIndex + 1}`, 1)"
65
- />
66
- </li>
67
- </ul>
68
- </dd>
69
- </dl>
70
- <AposAreaMenuItem
71
- v-else
72
- @click="add(item)"
73
- :item="item"
74
- @up="switchItem(`item-${itemIndex - 1}`, -1)"
75
- @down="switchItem(`item-${itemIndex + 1}`, 1)"
76
- />
77
- </li>
78
- </ul>
79
- </AposContextMenu>
80
- </div>
2
+ <AposButton
3
+ v-if="options.expanded"
4
+ :disabled="disabled"
5
+ v-bind="buttonOptions"
6
+ @click="openExpandedMenu(index)"
7
+ role="button"
8
+ />
9
+ <AposAreaContextualMenu
10
+ v-else
11
+ @add="$emit('add', $event);"
12
+ :button-options="buttonOptions"
13
+ :context-menu-options="contextMenuOptions"
14
+ :empty="true"
15
+ :index="index"
16
+ :widget-options="options.widgets"
17
+ :options="options"
18
+ :max-reached="maxReached"
19
+ :disabled="disabled"
20
+ />
81
21
  </template>
82
22
 
83
23
  <script>
84
-
85
- import cuid from 'cuid';
86
-
87
24
  export default {
25
+ name: 'AposAreaMenu',
88
26
  props: {
27
+ disabled: {
28
+ type: Boolean,
29
+ default: false
30
+ },
89
31
  empty: {
90
32
  type: Boolean,
91
33
  default: false
@@ -98,30 +40,24 @@ export default {
98
40
  type: Number,
99
41
  default: 0
100
42
  },
101
- widgetOptions: {
43
+ options: {
102
44
  type: Object,
103
45
  required: true
104
46
  },
105
47
  maxReached: {
106
48
  type: Boolean
107
49
  },
108
- disabled: {
109
- type: Boolean,
110
- default: false
50
+ // NOTE: Left for backwards compatibility.
51
+ // Should use options now instead.
52
+ widgetOptions: {
53
+ type: Object,
54
+ default: function() {
55
+ return {};
56
+ }
111
57
  }
112
58
  },
113
- emits: [ 'menu-close', 'menu-open', 'add' ],
114
- data() {
115
- return {
116
- active: 0,
117
- groupIsFocused: false,
118
- inContext: true
119
- };
120
- },
59
+ emits: [ 'add' ],
121
60
  computed: {
122
- moduleOptions() {
123
- return window.apos.area;
124
- },
125
61
  buttonOptions() {
126
62
  return {
127
63
  label: 'apostrophe:addContent',
@@ -131,147 +67,18 @@ export default {
131
67
  modifiers: this.empty ? [] : [ 'round', 'tiny' ],
132
68
  iconSize: this.empty ? 20 : 11
133
69
  };
134
- },
135
- isDisabled() {
136
- let flag = this.disabled;
137
- if (this.maxReached) {
138
- flag = true;
139
- }
140
- return flag;
141
- },
142
- extendedContextMenuOptions() {
143
- const modifiers = [ 'unpadded' ];
144
- if (!this.groupedMenus) {
145
- modifiers.push('tb-padded');
146
- }
147
- return {
148
- menuPlacement: 'bottom',
149
- menuOffset: 15,
150
- ...this.contextMenuOptions,
151
- modifiers
152
- };
153
- },
154
- groupedMenus() {
155
- let flag = false;
156
- this.contextMenuOptions.menu.forEach((e) => {
157
- if (e.items) {
158
- flag = true;
159
- }
160
- });
161
- return flag;
162
- },
163
- myMenu() {
164
- const clipboard = apos.area.widgetClipboard.get();
165
- const menu = [ ...this.contextMenuOptions.menu ];
166
- if (clipboard) {
167
- const widget = clipboard;
168
- const matchingChoice = menu.find(option => option.name === widget.type);
169
- if (matchingChoice) {
170
- menu.unshift({
171
- type: 'clipboard',
172
- ...matchingChoice,
173
- label: {
174
- key: 'apostrophe:pasteWidget',
175
- widget: this.$t(matchingChoice.label)
176
- },
177
- clipboard: widget
178
- });
179
- }
180
- }
181
- if (this.groupedMenus) {
182
- return this.composeGroups(menu);
183
- } else {
184
- return menu;
185
- }
186
- },
187
- menuId() {
188
- return `areaMenu-${cuid()}`;
189
70
  }
190
71
  },
191
- mounted() {
192
- // if this area is not in-context then it is assumed in a schema's modal and we need to bump
193
- // the z-index of menus above them
194
- this.inContext = !apos.util.closest(this.$el, '[data-apos-schema-area]');
195
- },
196
72
  methods: {
197
- menuClose(e) {
198
- this.$emit('menu-close', e);
199
- },
200
- menuOpen(e) {
201
- this.$emit('menu-open', e);
202
- },
203
- async add(item) {
204
- // Potential TODO: If we find ourselves manually flipping these bits in other AposContextMenu overrides
205
- // we should consider refactoring contextmenus to be able to self close when any click takes place within their el
206
- // as it is often the logical experience (not always, see tag menus and filters)
207
- this.$refs.contextMenu.isOpen = false;
208
- this.$emit('add', {
209
- ...item,
210
- index: this.index
211
- });
212
- },
213
- groupFocused() {
214
- this.groupIsFocused = true;
215
- },
216
- groupBlurred() {
217
- this.groupIsFocused = false;
218
- },
219
- composeGroups(menu) {
220
- const ungrouped = {
221
- label: 'apostrophe:ungroupedWidgets',
222
- items: []
223
- };
224
- const myMenu = [];
225
-
226
- menu.forEach((item) => {
227
- if (!item.items) {
228
- ungrouped.items.push(item);
229
- } else {
230
- myMenu.push(item);
231
- }
73
+ async openExpandedMenu(index) {
74
+ const data = await apos.modal.execute('AposAreaExpandedMenu', {
75
+ field: this.field,
76
+ options: this.options,
77
+ index
232
78
  });
233
79
 
234
- if (ungrouped.items.length) {
235
- myMenu.push(ungrouped);
236
- }
237
- return myMenu;
238
- },
239
-
240
- toggleGroup(index) {
241
- if (this.active !== index) {
242
- this.active = index;
243
- } else {
244
- this.active = null;
245
- }
246
- },
247
-
248
- switchGroup(index, dir) {
249
- let target;
250
-
251
- if (dir > 0) {
252
- target = index < this.$refs.groupButton.length - 1 ? index + 1 : 0;
253
- }
254
-
255
- if (dir < 0) {
256
- target = index === 0 ? this.$refs.groupButton.length - 1 : index - 1;
257
- }
258
-
259
- if (dir === 0) {
260
- target = 0;
261
- }
262
-
263
- if (!dir) {
264
- target = this.$refs.groupButton.length - 1;
265
- }
266
-
267
- this.$nextTick(() => {
268
- this.$refs.groupButton[target].focus();
269
- });
270
- },
271
-
272
- switchItem(name, dir) {
273
- if (this.$refs[name]) {
274
- this.$refs[name][0].querySelector('button').focus();
80
+ if (data) {
81
+ this.$emit('add', data);
275
82
  }
276
83
  }
277
84
  }
@@ -60,7 +60,8 @@
60
60
  @menu-close="toggleMenuFocus($event, 'top', false)"
61
61
  :context-menu-options="contextMenuOptions"
62
62
  :index="i"
63
- :widget-options="options.widgets"
63
+ :widget-options="widgets"
64
+ :options="options"
64
65
  :disabled="disabled"
65
66
  />
66
67
  </div>
@@ -96,10 +97,10 @@
96
97
  <component
97
98
  v-if="isContextual && !foreign"
98
99
  :is="widgetEditorComponent(widget.type)"
100
+ :options="widgetOptions"
101
+ :type="widget.type"
99
102
  :value="widget"
100
103
  @update="$emit('update', $event)"
101
- :options="options.widgets[widget.type]"
102
- :type="widget.type"
103
104
  :doc-id="docId"
104
105
  :focused="focused"
105
106
  :key="generation"
@@ -107,7 +108,7 @@
107
108
  <component
108
109
  v-else
109
110
  :is="widgetComponent(widget.type)"
110
- :options="options.widgets[widget.type]"
111
+ :options="widgetOptions"
111
112
  :type="widget.type"
112
113
  :id="widget._id"
113
114
  :area-field-id="fieldId"
@@ -128,7 +129,8 @@
128
129
  @add="$emit('add', $event)"
129
130
  :context-menu-options="bottomContextMenuOptions"
130
131
  :index="i + 1"
131
- :widget-options="options.widgets"
132
+ :widget-options="widgets"
133
+ :options="options"
132
134
  :disabled="disabled"
133
135
  @menu-open="toggleMenuFocus($event, 'bottom', true)"
134
136
  @menu-close="toggleMenuFocus($event, 'bottom', false)"
@@ -139,7 +141,6 @@
139
141
  </template>
140
142
 
141
143
  <script>
142
-
143
144
  import { klona } from 'klona';
144
145
  import AposIndicator from '../../../../ui/ui/apos/components/AposIndicator.vue';
145
146
 
@@ -249,7 +250,8 @@ export default {
249
250
  breadcrumbs: {
250
251
  $lastEl: null,
251
252
  list: []
252
- }
253
+ },
254
+ widgets: this.options.widgets || {}
253
255
  };
254
256
  },
255
257
  computed: {
@@ -262,6 +264,9 @@ export default {
262
264
  widgetLabel() {
263
265
  return window.apos.modules[`${this.widget.type}-widget`].label;
264
266
  },
267
+ widgetOptions() {
268
+ return this.widgets[this.widget.type];
269
+ },
265
270
  isContextual() {
266
271
  return this.moduleOptions.widgetIsContextual[this.widget.type];
267
272
  },
@@ -334,6 +339,16 @@ export default {
334
339
  }
335
340
  }
336
341
  },
342
+ created() {
343
+ if (this.options.groups) {
344
+ for (const group of Object.keys(this.options.groups)) {
345
+ this.widgets = {
346
+ ...this.options.groups[group].widgets,
347
+ ...this.widgets
348
+ };
349
+ }
350
+ }
351
+ },
337
352
  mounted() {
338
353
  // AposAreaEditor is listening for keyboard input that triggers
339
354
  // a 'focus my parent' plea
@@ -232,16 +232,17 @@ export default {
232
232
  } catch (e) {
233
233
  this.error = e.message || 'An error occurred. Please try again.';
234
234
  this.phase = 'beforeSubmit';
235
- this.requirements = getRequirements();
236
235
  } finally {
237
236
  this.busy = false;
238
237
  }
239
238
  },
240
239
  getInitialSubmitRequirementsData() {
241
- return Object.fromEntries(this.requirements.filter(r => r.phase !== 'afterPasswordVerified').map(r => ([
242
- r.name,
243
- r.value
244
- ])));
240
+ return Object.fromEntries(this.requirements
241
+ .filter(r => r.phase !== 'afterPasswordVerified' || !r.done)
242
+ .map(r => ([
243
+ r.name,
244
+ r.value
245
+ ])));
245
246
  },
246
247
  async invokeFinalLoginApi() {
247
248
  try {
@@ -257,7 +258,6 @@ export default {
257
258
  this.redirectAfterLogin();
258
259
  } catch (e) {
259
260
  this.error = e.message || 'An error occurred. Please try again.';
260
- this.requirements = getRequirements();
261
261
  this.phase = 'beforeSubmit';
262
262
  } finally {
263
263
  this.busy = false;