Skip to content

Commit e4e5f42

Browse files
committed
feat: Expose formatting menu bar actions through slash command
Signed-off-by: Julius Härtl <[email protected]>
1 parent 67bc06e commit e4e5f42

File tree

4 files changed

+110
-19
lines changed

4 files changed

+110
-19
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@
149149
],
150150
"moduleNameMapper": {
151151
"^@/(.*)$": "<rootDir>/src/$1",
152-
"\\.(css)$": "identity-obj-proxy"
152+
"^.+\\.(css|less|scss)$": "identity-obj-proxy"
153153
},
154154
"testPathIgnorePatterns": [
155155
"<rootDir>/src/tests/fixtures/",

src/components/Suggestion/LinkPicker/LinkPickerList.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@
2424
:items="items"
2525
@select="(item) => $emit('select', item)">
2626
<template #default="{ item }">
27-
<div class="link-picker__item">
28-
<img :src="item.icon">
27+
<div class="link-picker__item" :data-key="item.key">
28+
<compoent :is="item.icon" v-if="typeof item.icon !== 'string'" />
29+
<img v-else :src="item.icon">
2930
<div>{{ item.label }}</div>
3031
</div>
3132
</template>

src/components/Suggestion/LinkPicker/suggestions.js

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,50 @@
2121

2222
import createSuggestions from '../suggestions.js'
2323
import LinkPickerList from './LinkPickerList.vue'
24-
2524
import { searchProvider, getLinkWithPicker } from '@nextcloud/vue/dist/Components/NcRichText.js'
25+
import menuEntries from './../../Menu/entries.js'
26+
import { getIsActive } from '../../Menu/utils.js'
27+
28+
const suggestGroupFormat = t('text', 'Formatting')
29+
const suggestGroupPicker = t('text', 'Smart picker')
30+
31+
const filterOut = (e) => {
32+
return ['undo', 'redo', 'outline', 'emoji-picker'].indexOf(e.key) > -1
33+
}
34+
35+
const important = ['task-list', 'table']
36+
37+
const sortImportantFirst = (list) => {
38+
return [
39+
...list.filter(e => important.indexOf(e.key) > -1),
40+
...list.filter(e => important.indexOf(e.key) === -1),
41+
]
42+
}
43+
44+
const formattingSuggestions = (query) => {
45+
return sortImportantFirst(
46+
[
47+
...menuEntries.find(e => e.key === 'headings').children,
48+
...menuEntries.filter(e => e.action && !filterOut(e)),
49+
...menuEntries.find(e => e.key === 'callouts').children,
50+
{
51+
...menuEntries.find(e => e.key === 'emoji-picker'),
52+
action: (command) => command.insertContent(':'),
53+
},
54+
].filter(e => e?.label?.toLowerCase?.()?.includes(query.toLowerCase()))
55+
.map(e => ({ ...e, suggestGroup: suggestGroupFormat })),
56+
)
57+
}
2658

2759
export default () => createSuggestions({
2860
listComponent: LinkPickerList,
2961
command: ({ editor, range, props }) => {
62+
if (props.action) {
63+
const commandChain = editor.chain().deleteRange(range)
64+
props.action(commandChain)
65+
commandChain.run()
66+
return
67+
}
3068
getLinkWithPicker(props.providerId, true)
3169
.then(link => {
3270
editor
@@ -39,14 +77,23 @@ export default () => createSuggestions({
3977
console.error('Smart picker promise rejected', error)
4078
})
4179
},
42-
items: ({ query }) => {
43-
return searchProvider(query)
44-
.map(p => {
45-
return {
46-
label: p.title,
47-
icon: p.icon_url,
48-
providerId: p.id,
49-
}
50-
})
80+
items: ({ editor, query }) => {
81+
return [
82+
...formattingSuggestions(query)
83+
.filter(({ action, isActive }) => {
84+
const canRunState = action(editor?.can())
85+
const isActiveState = isActive && getIsActive({ isActive }, editor)
86+
return canRunState && !isActiveState
87+
}),
88+
...searchProvider(query)
89+
.map(p => {
90+
return {
91+
suggestGroup: suggestGroupPicker,
92+
label: p.title,
93+
icon: p.icon_url,
94+
providerId: p.id,
95+
}
96+
}),
97+
]
5198
},
5299
})

src/components/Suggestion/SuggestionListWrapper.vue

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,17 @@
2323
<template>
2424
<div class="suggestion-list">
2525
<template v-if="hasResults">
26-
<div v-for="(item, index) in items"
27-
:key="index"
28-
class="suggestion-list__item"
29-
:class="{ 'is-selected': index === selectedIndex }"
30-
@click="selectItem(index)">
31-
<slot :item="item" :active="index === selectedIndex" />
26+
<div v-for="(groupItems, key, groupIndex) in itemGroups" :key="key">
27+
<div v-if="hasGroups" class="suggestion-list__group">
28+
{{ key }}
29+
</div>
30+
<div v-for="(item, index) in groupItems"
31+
:key="combineIndex(groupIndex, index)"
32+
class="suggestion-list__item"
33+
:class="{ 'is-selected': combineIndex(groupIndex, index) === selectedIndex }"
34+
@click="selectItem(combineIndex(groupIndex, index))">
35+
<slot :item="item" :active="combineIndex(groupIndex, index) === selectedIndex" />
36+
</div>
3237
</div>
3338
</template>
3439
<div v-else class="suggestion-list__item is-empty">
@@ -56,6 +61,9 @@ export default {
5661
}
5762
},
5863
computed: {
64+
hasGroups() {
65+
return Object.keys(this.itemGroups).includes(undefined)
66+
},
5967
hasResults() {
6068
return this.items.length > 0
6169
},
@@ -68,6 +76,26 @@ export default {
6876
return this.selectedIndex * this.itemHeight >= this.$el.scrollTop
6977
&& (this.selectedIndex + 1) * this.itemHeight <= this.$el.scrollTop + this.$el.clientHeight
7078
},
79+
itemGroups() {
80+
const groups = {}
81+
this.items.forEach((item) => {
82+
if (!groups[item.suggestGroup]) {
83+
groups[item.suggestGroup] = []
84+
}
85+
groups[item.suggestGroup].push(item)
86+
})
87+
return groups
88+
},
89+
combineIndex() {
90+
return (groupIndex, index) => {
91+
const previousItemCount = Object.values(this.itemGroups)
92+
.slice(0, groupIndex)
93+
.reduce((sum, items) => {
94+
return sum + items.length
95+
}, 0)
96+
return previousItemCount + index
97+
}
98+
},
7199
},
72100
watch: {
73101
items() {
@@ -128,11 +156,26 @@ export default {
128156
129157
min-width: 200px;
130158
max-width: 400px;
159+
width: 80vw;
131160
padding: 4px;
132161
// Show maximum 5 entries and a half to show scroll
133162
max-height: 35.5px * 5 + 18px;
134163
margin: 5px 0;
135164
165+
&__group {
166+
font-weight: bold;
167+
color: var(--color-primary-element);
168+
font-size: var(--default-font-size);
169+
line-height: 44px;
170+
white-space: nowrap;
171+
overflow: hidden;
172+
text-overflow: ellipsis;
173+
opacity: .7;
174+
box-shadow: none !important;
175+
flex-shrink: 0;
176+
padding-left: 8px;
177+
}
178+
136179
&__item {
137180
border-radius: 8px;
138181
padding: 4px 8px;

0 commit comments

Comments
 (0)