apostrophe 3.33.0 → 3.35.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 (59) hide show
  1. package/.github/workflows/main.yml +3 -0
  2. package/CHANGELOG.md +31 -0
  3. package/defaults.js +2 -1
  4. package/index.js +2 -1
  5. package/modules/@apostrophecms/admin-bar/index.js +74 -0
  6. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +16 -6
  7. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextModeAndSettings.vue +24 -1
  8. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +15 -0
  9. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextUndoRedo.vue +20 -18
  10. package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +22 -1
  11. package/modules/@apostrophecms/area/ui/apos/components/AposAreaExpandedMenu.vue +35 -0
  12. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +6 -1
  13. package/modules/@apostrophecms/asset/lib/globalIcons.js +41 -17
  14. package/modules/@apostrophecms/command-menu/index.js +375 -0
  15. package/modules/@apostrophecms/command-menu/ui/apos/apps/AposCommandMenu.js +34 -0
  16. package/modules/@apostrophecms/command-menu/ui/apos/components/AposCommandMenuKey.vue +94 -0
  17. package/modules/@apostrophecms/command-menu/ui/apos/components/AposCommandMenuKeyList.vue +106 -0
  18. package/modules/@apostrophecms/command-menu/ui/apos/components/AposCommandMenuShortcut.vue +223 -0
  19. package/modules/@apostrophecms/command-menu/ui/apos/components/TheAposCommandMenu.vue +116 -0
  20. package/modules/@apostrophecms/doc/index.js +9 -0
  21. package/modules/@apostrophecms/doc-type/index.js +117 -1
  22. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +7 -0
  23. package/modules/@apostrophecms/i18n/i18n/de.json +446 -0
  24. package/modules/@apostrophecms/i18n/i18n/en.json +27 -0
  25. package/modules/@apostrophecms/i18n/i18n/es.json +19 -0
  26. package/modules/@apostrophecms/i18n/i18n/fr.json +19 -0
  27. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +19 -0
  28. package/modules/@apostrophecms/i18n/i18n/sk.json +19 -0
  29. package/modules/@apostrophecms/image/index.js +7 -0
  30. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +2 -0
  31. package/modules/@apostrophecms/image/ui/apos/components/AposMediaUploader.vue +11 -0
  32. package/modules/@apostrophecms/login/index.js +1 -1
  33. package/modules/@apostrophecms/modal/ui/apos/apps/AposModals.js +10 -1
  34. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +38 -3
  35. package/modules/@apostrophecms/modal/ui/apos/components/TheAposModals.vue +32 -2
  36. package/modules/@apostrophecms/page/index.js +43 -1
  37. package/modules/@apostrophecms/page/ui/apos/components/AposPagesManager.vue +4 -0
  38. package/modules/@apostrophecms/piece-type/index.js +145 -20
  39. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +5 -1
  40. package/modules/@apostrophecms/rich-text-widget/index.js +153 -5
  41. package/modules/@apostrophecms/rich-text-widget/ui/apos/apps/AposRichTextPermalinkResolver.js +28 -0
  42. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +88 -14
  43. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapAnchor.vue +253 -0
  44. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +134 -24
  45. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Anchor.js +59 -0
  46. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Default.js +12 -4
  47. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/ListItem.js +6 -0
  48. package/modules/@apostrophecms/schema/lib/addFieldTypes.js +17 -0
  49. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +4 -2
  50. package/modules/@apostrophecms/search/index.js +27 -28
  51. package/modules/@apostrophecms/template/views/outerLayoutBase.html +1 -0
  52. package/modules/@apostrophecms/ui/ui/apos/components/AposCheckbox.vue +1 -1
  53. package/modules/@apostrophecms/user/index.js +24 -8
  54. package/modules/@apostrophecms/util/index.js +13 -0
  55. package/package.json +2 -2
  56. package/test/command-menu.js +877 -0
  57. package/test/concurrent-array-relationships.js +0 -1
  58. package/test/users.js +21 -0
  59. package/test/utils/commands.js +204 -0
@@ -0,0 +1,375 @@
1
+ const assert = require('assert').strict;
2
+
3
+ module.exports = {
4
+ options: {
5
+ components: {},
6
+ alias: 'commandMenu'
7
+ },
8
+ commands(self) {
9
+ return {
10
+ add: {
11
+ [`${self.__meta.name}:show-shortcut-list`]: {
12
+ type: 'item',
13
+ label: 'apostrophe:commandMenuShowShortcutList',
14
+ action: {
15
+ type: 'open-modal',
16
+ payload: {
17
+ name: 'AposCommandMenuShortcut',
18
+ props: { moduleName: '@apostrophecms/command-menu' }
19
+ }
20
+ },
21
+ shortcut: '?'
22
+ }
23
+ },
24
+ modal: {
25
+ default: {
26
+ '@apostrophecms/command-menu:general': {
27
+ label: 'apostrophe:commandMenuGeneral',
28
+ commands: [
29
+ `${self.__meta.name}:show-shortcut-list`
30
+ ]
31
+ }
32
+ }
33
+ }
34
+ };
35
+ },
36
+ init(self) {
37
+ self.commands = {};
38
+ self.groups = {};
39
+ self.modals = {};
40
+
41
+ self.addShortcutModal();
42
+ self.enableBrowserData();
43
+ },
44
+ handlers(self) {
45
+ return {
46
+ 'apostrophe:ready': {
47
+ composeCommands() {
48
+ const definitions = Object.fromEntries(
49
+ Object.values(self.apos.modules)
50
+ .map(self.composeCommandsForModule)
51
+ .filter(([ , commands = [] ]) => commands.length)
52
+ );
53
+
54
+ try {
55
+ const composed = self.apos.util.pipe(self.composeRemoves, self.composeCommands, self.composeGroups, self.composeModals)({ definitions });
56
+
57
+ const validationResult = [].concat(
58
+ Object.entries(composed.commands)
59
+ .map(([ name, command ]) => self.validateCommand({
60
+ name,
61
+ command
62
+ })),
63
+ Object.entries(composed.groups)
64
+ .map(([ name, group ]) => self.validateGroup({
65
+ name,
66
+ group
67
+ })),
68
+ Object.entries(composed.modals)
69
+ .flatMap(([ name, modal ]) => self.validateModal({
70
+ name,
71
+ modal
72
+ }))
73
+ );
74
+ self.compileErrors(validationResult);
75
+
76
+ const built = self.apos.util.pipe(self.buildCommands, self.buildGroups, self.buildModals)({ composed });
77
+ self.commands = built.commands;
78
+ self.groups = built.groups;
79
+ self.modals = built.modals;
80
+ } catch (error) {
81
+ self.apos.util.error(error, 'Command-Menu validation error');
82
+ }
83
+ }
84
+ }
85
+ };
86
+ },
87
+ methods(self) {
88
+ return {
89
+ composeCommandsForModule(aposModule) {
90
+ return [
91
+ aposModule.__meta.name,
92
+ aposModule.__meta.chain
93
+ .map(entry => {
94
+ const metadata = aposModule.__meta.commands[entry.name] || null;
95
+
96
+ return typeof metadata === 'function'
97
+ ? metadata(aposModule)
98
+ : metadata;
99
+ })
100
+ .filter(entry => entry !== null)
101
+ ];
102
+ },
103
+ composeRemoves(initialState) {
104
+ const formatRemove = (state, chain) => {
105
+ return chain
106
+ .reduce(
107
+ (removes, { add = {}, remove = [] }) => {
108
+ const existingCommands = Object.keys(add);
109
+
110
+ return removes
111
+ .filter(key => !existingCommands.includes(key))
112
+ .concat(remove);
113
+ },
114
+ state
115
+ );
116
+ };
117
+
118
+ const concatenate = Object.values(initialState.definitions).reduce(formatRemove, []);
119
+
120
+ return {
121
+ ...initialState,
122
+ removes: concatenate || []
123
+ };
124
+ },
125
+ composeCommands(initialState) {
126
+ const formatCommands = (state, chain) => {
127
+ return chain
128
+ .reduce(
129
+ (commands, { add = {} }) => self.apos.util.merge(commands, add),
130
+ state
131
+ );
132
+ };
133
+
134
+ const concatenate = Object.values(initialState.definitions).reduce(formatCommands, {});
135
+
136
+ return {
137
+ ...initialState,
138
+ commands: concatenate || {}
139
+ };
140
+ },
141
+ composeGroups(initialState) {
142
+ const removeDuplicates = (left, right) => {
143
+ const commands = Object.values(right).flatMap(group => group.commands);
144
+
145
+ return Object.fromEntries(
146
+ Object.entries(left)
147
+ .map(([ name, group ]) => {
148
+ return [
149
+ name,
150
+ {
151
+ ...group,
152
+ commands: (group.commands || []).filter(command => !commands.includes(command))
153
+ }
154
+ ];
155
+ })
156
+ );
157
+ };
158
+
159
+ const formatGroups = (state, chain) => {
160
+ return chain
161
+ .reduce(
162
+ (groups, { group = {} }) => {
163
+ return self.apos.util.merge(removeDuplicates(groups, group), group);
164
+ },
165
+ state
166
+ );
167
+ };
168
+
169
+ const concatenate = Object.values(initialState.definitions).reduce(formatGroups, {});
170
+
171
+ return {
172
+ ...initialState,
173
+ groups: concatenate || {}
174
+ };
175
+ },
176
+ composeModals(initialState) {
177
+ const formatModals = (state, chain) => {
178
+ return chain
179
+ .reduce(
180
+ (modals, { modal = {} }) => self.apos.util.merge(modals, modal),
181
+ state
182
+ );
183
+ };
184
+
185
+ const concatenate = Object.values(initialState.definitions).reduce(formatModals, {});
186
+
187
+ return {
188
+ ...initialState,
189
+ modals: concatenate || {}
190
+ };
191
+ },
192
+ validateCommand({ name, command }) {
193
+ try {
194
+ assert.equal(command.type, 'item', `Invalid command type, must be "item", for ${name}`);
195
+ command.label && typeof command.label === 'object'
196
+ ? assert.equal(typeof command.label.key, 'string', `Invalid command label key for ${name}`)
197
+ : assert.equal(typeof command.label, 'string', `Invalid command label, must be a string, for ${name} "${typeof command.label}" provided`);
198
+ assert.equal(typeof command.action, 'object', `Invalid command action, must be an object for ${name}`) &&
199
+ assert.equal(typeof command.action.type, 'string', `Invalid command action type for ${name}`) &&
200
+ assert.equal(typeof command.action.payload, 'object', `Invalid command action payload for ${name}`);
201
+ command.permission && (
202
+ assert.equal(typeof command.permission, 'object', `Invalid command permission for ${name}`) &&
203
+ assert.equal(typeof command.permission.action, 'string', `Invalid command permission action for ${name}`) &&
204
+ assert.equal(typeof command.permission.type, 'string', `Invalid command permission type for ${name}`)
205
+ );
206
+ command.modal &&
207
+ assert.equal(typeof command.modal, 'string', `Invalid command modal for ${name}`);
208
+ assert.equal(typeof command.shortcut, 'string', `Invalid command shortcut, must be a string, for ${name}`);
209
+
210
+ return [ true, null ];
211
+ } catch (error) {
212
+ return [ false, error ];
213
+ }
214
+ },
215
+ validateGroup({ name, group }) {
216
+ try {
217
+ group.label && typeof group.label === 'object'
218
+ ? assert.equal(typeof group.label.key, 'string', `Invalid group label key for ${name}`)
219
+ : assert.equal(typeof group.label, 'string', `Invalid group label, must be a string, for ${name} "${typeof group.label}" provided`);
220
+ assert.equal(Array.isArray(group.commands), true, `Invalid group commands, must be an array for ${name}`);
221
+ assert.ok(group.commands.every(field => typeof field === 'string'), `Invalid group commands, must contains strings, for ${name}`);
222
+
223
+ return [ true, null ];
224
+ } catch (error) {
225
+ return [ false, error ];
226
+ }
227
+ },
228
+ validateModal({ name, modal }) {
229
+ return Object.entries(modal)
230
+ .map(([ groupName, group ]) => self.validateGroup({
231
+ name: `${name}:${groupName}`,
232
+ group
233
+ }));
234
+ },
235
+ compileErrors(result) {
236
+ const errors = result
237
+ .filter(([ success ]) => !success)
238
+ .map(([ , error ]) => error);
239
+ if (errors.length) {
240
+ const error = new Error('Invalid', { cause: errors });
241
+ // For bc with node 14 and below we need to check cause
242
+ if (!error.cause) {
243
+ error.cause = errors;
244
+ }
245
+ throw error;
246
+ }
247
+ },
248
+ buildCommands(initialState) {
249
+ const concatenate = self.apos.util.omit(
250
+ initialState.composed.commands,
251
+ initialState.composed.removes
252
+ );
253
+
254
+ return {
255
+ ...initialState,
256
+ commands: concatenate || {}
257
+ };
258
+ },
259
+ buildGroups(initialState) {
260
+ const filterGroups = (state, [ name, group ]) => {
261
+ const commands = group.commands
262
+ .map(field => [ field, initialState.commands[field] ])
263
+ .filter(([ , isNotEmpty ]) => isNotEmpty);
264
+
265
+ return commands.length
266
+ ? {
267
+ ...state,
268
+ [name]: {
269
+ ...group,
270
+ commands: Object.fromEntries(commands)
271
+ }
272
+ }
273
+ : state;
274
+ };
275
+
276
+ const concatenate = Object.entries(initialState.composed.groups).reduce(filterGroups, {});
277
+
278
+ return {
279
+ ...initialState,
280
+ groups: concatenate || {}
281
+ };
282
+ },
283
+ buildModals(initialState) {
284
+ const formatModals = (state, [ modal, groups ]) => {
285
+ const built = self.buildGroups({
286
+ commands: initialState.commands,
287
+ composed: { groups }
288
+ });
289
+
290
+ return {
291
+ ...state,
292
+ [modal]: built.groups
293
+ };
294
+ };
295
+
296
+ const concatenate = Object.entries(initialState.composed.modals).reduce(formatModals, {});
297
+
298
+ return {
299
+ ...initialState,
300
+ modals: concatenate || {}
301
+ };
302
+ },
303
+ isCommandVisible(req, command) {
304
+ return command.permission
305
+ ? self.apos.permission.can(req, command.permission.action, command.permission.type, command.permission.mode || 'draft')
306
+ : true;
307
+ },
308
+ getVisibleGroups(visibleCommands, groups = self.groups) {
309
+ const formatGroup = (state, [ name, field ]) =>
310
+ visibleCommands.includes(name)
311
+ ? {
312
+ ...state,
313
+ [name]: field
314
+ }
315
+ : state;
316
+
317
+ return Object.fromEntries(
318
+ Object.entries(groups)
319
+ .map(([ key, group ]) => {
320
+ const commands = Object.entries(group.commands).reduce(formatGroup, {});
321
+
322
+ return [
323
+ key,
324
+ {
325
+ ...group,
326
+ commands
327
+ }
328
+ ];
329
+ })
330
+ .filter(([ , { commands = {} } ]) => Object.keys(commands).length)
331
+ );
332
+ },
333
+ getVisibleModals(visibleCommands, modals = self.modals) {
334
+ return Object.fromEntries(
335
+ Object.entries(modals)
336
+ .map(([ key, groups ]) => [ key, self.getVisibleGroups(visibleCommands, groups) ])
337
+ .filter(([ , groups ]) => Object.keys(groups).length)
338
+ );
339
+ },
340
+ getVisible(req) {
341
+ const visibleCommands = Object.entries(self.commands)
342
+ .map(([ key, command ]) => self.isCommandVisible(req, command) ? key : null)
343
+ .filter(isNotEmpty => isNotEmpty);
344
+
345
+ const groups = self.getVisibleGroups(visibleCommands);
346
+ const modals = self.getVisibleModals(visibleCommands);
347
+
348
+ return {
349
+ groups,
350
+ modals
351
+ };
352
+ },
353
+ addShortcutModal() {
354
+ self.apos.modal.add(
355
+ `${self.__meta.name}:shortcut`,
356
+ self.getComponentName('shortcutModal', 'AposCommandMenuShortcut'),
357
+ { moduleName: self.__meta.name }
358
+ );
359
+ },
360
+ getBrowserData(req) {
361
+ if (!req.user) {
362
+ return false;
363
+ }
364
+
365
+ const { groups, modals } = self.getVisible(req);
366
+
367
+ return {
368
+ components: { the: self.options.components.the || 'TheAposCommandMenu' },
369
+ groups,
370
+ modals
371
+ };
372
+ }
373
+ };
374
+ }
375
+ };
@@ -0,0 +1,34 @@
1
+ import Vue from 'Modules/@apostrophecms/ui/lib/vue';
2
+
3
+ export default function() {
4
+ // Careful, login page is in user scene but has no command menu
5
+ if (apos.commandMenu) {
6
+ const theAposCommandMenu = new Vue({
7
+ el: '#apos-command-menu',
8
+ computed: {
9
+ apos () {
10
+ return window.apos;
11
+ }
12
+ },
13
+ methods: {
14
+ getModal() {
15
+ return this.$refs.commandMenu.modal;
16
+ }
17
+ },
18
+ render(h) {
19
+ return h(
20
+ apos.commandMenu.components.the,
21
+ {
22
+ ref: 'commandMenu',
23
+ props: {
24
+ groups: apos.commandMenu.groups,
25
+ modals: apos.commandMenu.modals
26
+ }
27
+ }
28
+ );
29
+ }
30
+ });
31
+
32
+ apos.commandMenu.getModal = theAposCommandMenu.getModal;
33
+ }
34
+ }
@@ -0,0 +1,94 @@
1
+ <template>
2
+ <span :class="classes">
3
+ <AposIndicator
4
+ v-if="icon"
5
+ :icon="icon"
6
+ :icon-size="iconSize"
7
+ class="apos-button__icon"
8
+ fill-color="color"
9
+ />
10
+ <slot name="label" v-if="label">
11
+ {{ $t(label ) }}
12
+ </slot>
13
+ </span>
14
+ </template>
15
+
16
+ <script>
17
+ export default {
18
+ name: 'AposCommandMenuKey',
19
+ props: {
20
+ label: {
21
+ type: [ String, Object ],
22
+ default: null
23
+ },
24
+ color: {
25
+ type: String,
26
+ default: null
27
+ },
28
+ textColor: {
29
+ type: String,
30
+ default: null
31
+ },
32
+ icon: {
33
+ type: String,
34
+ default: null
35
+ },
36
+ iconSize: {
37
+ type: Number,
38
+ default: 12
39
+ },
40
+ textOnly: {
41
+ type: Boolean,
42
+ default: false
43
+ }
44
+ },
45
+ computed: {
46
+ classes() {
47
+ return [
48
+ this.textOnly ? 'apos-command-menu-text' : 'apos-command-menu-key',
49
+ !this.icon && this.label.length > 1 ? 'apos-command-menu-key-auto' : ''
50
+ ];
51
+ }
52
+ }
53
+ };
54
+ </script>
55
+
56
+ <style lang="scss" scoped>
57
+
58
+ .apos-command-menu-key {
59
+ @include type-small;
60
+ display: inline-flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ box-sizing: border-box;
64
+ width: $spacing-double;
65
+ height: $spacing-double;
66
+ padding: 3px $spacing-half;
67
+ margin-left: $spacing-half;
68
+ border: 1px solid var(--a-base-7);
69
+ border-radius: 3px;
70
+ border-color: var(--a-base-7);
71
+ border-bottom: 2px solid var(--a-base-7);
72
+ color: var(--a-base-1);
73
+ background: linear-gradient(180deg, var(--a-base-10) 0%, var(--a-base-9) 100%);
74
+ font-weight: 600;
75
+
76
+ &.apos-command-menu-key-auto {
77
+ width: auto;
78
+ }
79
+ }
80
+
81
+ .apos-command-menu-text {
82
+ display: inline-flex;
83
+ align-items: center;
84
+ justify-content: center;
85
+ box-sizing: border-box;
86
+ width: auto;
87
+ height: $spacing-double;
88
+ padding: 3px 2px;
89
+ margin-left: $spacing-half;
90
+ @include type-small;
91
+ color: var(--a-base-1);
92
+ }
93
+
94
+ </style>
@@ -0,0 +1,106 @@
1
+ <template>
2
+ <div class="apos-command-menu-keys">
3
+ <AposCommandMenuKey
4
+ v-for="(key, index) in keys"
5
+ :key="index"
6
+ :label="key.label"
7
+ :icon="key.icon"
8
+ :text-only="key.textOnly"
9
+ />
10
+ </div>
11
+ </template>
12
+
13
+ <script>
14
+ export default {
15
+ name: 'AposCommandMenuKeyList',
16
+ props: {
17
+ shortcut: {
18
+ type: String,
19
+ default: ''
20
+ }
21
+ },
22
+ computed: {
23
+ keys() {
24
+ const [ shortcut ] = this.shortcut.split(' ');
25
+ const iconMapping = {
26
+ ' ': 'keyboard-space',
27
+ alt: 'apple-keyboard-option',
28
+ arrowdown: 'arrow-down-icon',
29
+ arrowleft: 'arrow-left-icon',
30
+ arrowright: 'arrow-right-icon',
31
+ arrowup: 'arrow-up-icon',
32
+ backspace: 'keyboard-backspace',
33
+ caps: 'apple-keyboard-caps',
34
+ capslock: 'apple-keyboard-caps',
35
+ cmd: 'apple-keyboard-command',
36
+ command: 'apple-keyboard-command',
37
+ control: 'apple-keyboard-control',
38
+ ctrl: 'apple-keyboard-control',
39
+ enter: 'keyboard-return',
40
+ esc: 'keyboard-esc',
41
+ escape: 'keyboard-esc',
42
+ f10: 'keyboard-f10',
43
+ f11: 'keyboard-f11',
44
+ f12: 'keyboard-f12',
45
+ f1: 'keyboard-f1',
46
+ f2: 'keyboard-f2',
47
+ f3: 'keyboard-f3',
48
+ f4: 'keyboard-f4',
49
+ f5: 'keyboard-f5',
50
+ f6: 'keyboard-f6',
51
+ f7: 'keyboard-f7',
52
+ f8: 'keyboard-f8',
53
+ f9: 'keyboard-f9',
54
+ meta: 'apple-keyboard-command',
55
+ option: 'apple-keyboard-option',
56
+ return: 'keyboard-return',
57
+ shift: 'apple-keyboard-shift',
58
+ space: 'keyboard-space',
59
+ tab: 'keyboard-tab'
60
+ };
61
+ const keyMapping = {
62
+ delete: 'del',
63
+ pagedown: 'pgdn',
64
+ pageup: 'pgup'
65
+ };
66
+
67
+ return shortcut
68
+ .split('+')
69
+ .flatMap(this.then)
70
+ .map(key => {
71
+ if (key === 'then') {
72
+ return {
73
+ icon: null,
74
+ label: 'apostrophe:commandMenuKeyThen',
75
+ textOnly: true
76
+ };
77
+ }
78
+
79
+ const icon = iconMapping[key.toLowerCase()];
80
+
81
+ return {
82
+ icon,
83
+ label: icon
84
+ ? null
85
+ : (keyMapping[key.toLowerCase()] || key).toLowerCase()
86
+ };
87
+ });
88
+ }
89
+ },
90
+ methods: {
91
+ then(keys) {
92
+ return keys.includes(',')
93
+ ? keys.split(',').flatMap(key => [ key, 'then' ]).slice(0, -1)
94
+ : keys;
95
+ }
96
+ }
97
+ };
98
+ </script>
99
+
100
+ <style lang="scss" scoped>
101
+
102
+ .apos-command-menu-keys {
103
+ display: inline-flex;
104
+ }
105
+
106
+ </style>