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.
- package/.github/workflows/main.yml +3 -0
- package/CHANGELOG.md +31 -0
- package/defaults.js +2 -1
- package/index.js +2 -1
- package/modules/@apostrophecms/admin-bar/index.js +74 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +16 -6
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextModeAndSettings.vue +24 -1
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +15 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextUndoRedo.vue +20 -18
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +22 -1
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaExpandedMenu.vue +35 -0
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +6 -1
- package/modules/@apostrophecms/asset/lib/globalIcons.js +41 -17
- package/modules/@apostrophecms/command-menu/index.js +375 -0
- package/modules/@apostrophecms/command-menu/ui/apos/apps/AposCommandMenu.js +34 -0
- package/modules/@apostrophecms/command-menu/ui/apos/components/AposCommandMenuKey.vue +94 -0
- package/modules/@apostrophecms/command-menu/ui/apos/components/AposCommandMenuKeyList.vue +106 -0
- package/modules/@apostrophecms/command-menu/ui/apos/components/AposCommandMenuShortcut.vue +223 -0
- package/modules/@apostrophecms/command-menu/ui/apos/components/TheAposCommandMenu.vue +116 -0
- package/modules/@apostrophecms/doc/index.js +9 -0
- package/modules/@apostrophecms/doc-type/index.js +117 -1
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +7 -0
- package/modules/@apostrophecms/i18n/i18n/de.json +446 -0
- package/modules/@apostrophecms/i18n/i18n/en.json +27 -0
- package/modules/@apostrophecms/i18n/i18n/es.json +19 -0
- package/modules/@apostrophecms/i18n/i18n/fr.json +19 -0
- package/modules/@apostrophecms/i18n/i18n/pt-BR.json +19 -0
- package/modules/@apostrophecms/i18n/i18n/sk.json +19 -0
- package/modules/@apostrophecms/image/index.js +7 -0
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +2 -0
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaUploader.vue +11 -0
- package/modules/@apostrophecms/login/index.js +1 -1
- package/modules/@apostrophecms/modal/ui/apos/apps/AposModals.js +10 -1
- package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +38 -3
- package/modules/@apostrophecms/modal/ui/apos/components/TheAposModals.vue +32 -2
- package/modules/@apostrophecms/page/index.js +43 -1
- package/modules/@apostrophecms/page/ui/apos/components/AposPagesManager.vue +4 -0
- package/modules/@apostrophecms/piece-type/index.js +145 -20
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +5 -1
- package/modules/@apostrophecms/rich-text-widget/index.js +153 -5
- package/modules/@apostrophecms/rich-text-widget/ui/apos/apps/AposRichTextPermalinkResolver.js +28 -0
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +88 -14
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapAnchor.vue +253 -0
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +134 -24
- package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Anchor.js +59 -0
- package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Default.js +12 -4
- package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/ListItem.js +6 -0
- package/modules/@apostrophecms/schema/lib/addFieldTypes.js +17 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +4 -2
- package/modules/@apostrophecms/search/index.js +27 -28
- package/modules/@apostrophecms/template/views/outerLayoutBase.html +1 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposCheckbox.vue +1 -1
- package/modules/@apostrophecms/user/index.js +24 -8
- package/modules/@apostrophecms/util/index.js +13 -0
- package/package.json +2 -2
- package/test/command-menu.js +877 -0
- package/test/concurrent-array-relationships.js +0 -1
- package/test/users.js +21 -0
- 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>
|