frappe-ui 0.1.145 → 0.1.147
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/package.json +2 -1
- package/src/components/Button/Button.vue +1 -1
- package/src/components/Charts/Charts.story.vue +4 -0
- package/src/components/Charts/axisChartOptions.ts +54 -74
- package/src/components/Charts/eChartOptions.ts +16 -4
- package/src/components/Charts/helpers.ts +31 -0
- package/src/components/Charts/types.ts +21 -0
- package/src/components/FormControl.vue +8 -11
- package/src/components/Input.vue +1 -1
- package/src/components/ListView/ListView.vue +1 -0
- package/src/components/TextEditor/TextEditor.vue +45 -4
- package/src/components/TextEditor/{EmojiList.vue → extensions/emoji/EmojiList.vue} +15 -10
- package/src/components/TextEditor/extensions/emoji/emoji-extension.ts +55 -0
- package/src/components/TextEditor/extensions/heading/heading.ts +20 -0
- package/src/components/TextEditor/{SlashCommandsList.vue → extensions/slash-commands/SlashCommandsList.vue} +22 -10
- package/src/components/TextEditor/extensions/slash-commands/slash-commands-extension.ts +155 -0
- package/src/components/TextEditor/{SuggestionList.vue → extensions/suggestion/SuggestionList.vue} +4 -7
- package/src/components/TextEditor/extensions/suggestion/createSuggestionExtension.ts +151 -0
- package/src/components/TextEditor/extensions/tag/tag-extension.ts +155 -0
- package/src/components/TextEditor/emoji-extension.js +0 -100
- package/src/components/TextEditor/slash-commands-extension.ts +0 -244
- /package/src/components/TextEditor/{emojis.json → extensions/emoji/emojis.json} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frappe-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.147",
|
|
4
4
|
"description": "A set of components and utilities for rapid UI development",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"@tiptap/extension-code-block": "^2.11.9",
|
|
38
38
|
"@tiptap/extension-code-block-lowlight": "^2.11.5",
|
|
39
39
|
"@tiptap/extension-color": "^2.0.3",
|
|
40
|
+
"@tiptap/extension-heading": "^2.12.0",
|
|
40
41
|
"@tiptap/extension-highlight": "^2.0.3",
|
|
41
42
|
"@tiptap/extension-image": "^2.0.3",
|
|
42
43
|
"@tiptap/extension-link": "^2.0.3",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
<component v-else-if="icon" :is="icon" :class="slotClasses" />
|
|
37
37
|
<slot name="icon" v-else-if="$slots.icon" />
|
|
38
38
|
</template>
|
|
39
|
-
<span v-else :class="{ 'sr-only': isIconButton }">
|
|
39
|
+
<span v-else :class="{ 'sr-only': isIconButton }" class="truncate">
|
|
40
40
|
<slot>{{ label }}</slot>
|
|
41
41
|
</span>
|
|
42
42
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import useEchartsOptions from './eChartOptions'
|
|
2
|
-
import { formatValue } from './helpers'
|
|
2
|
+
import { formatValue, mergeDeep } from './helpers'
|
|
3
3
|
import {
|
|
4
4
|
AreaSeriesConfig,
|
|
5
5
|
AxisChartConfig,
|
|
@@ -33,61 +33,60 @@ export default function useAxisChartOptions(config: AxisChartConfig) {
|
|
|
33
33
|
baseOptions.yAxis[1].show = true
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const _val = swapXY ? params.value?.[0] : params.value?.[1]
|
|
67
|
-
return formatValue(_val, 1, true)
|
|
68
|
-
},
|
|
69
|
-
fontSize: 11,
|
|
70
|
-
},
|
|
71
|
-
labelLayout: { hideOverlap: true },
|
|
72
|
-
itemStyle: {
|
|
73
|
-
color: s.color,
|
|
36
|
+
baseOptions.series = config.series.map((s, idx) => {
|
|
37
|
+
let labelPosition = 'top'
|
|
38
|
+
if (s.type == 'bar' && config.stacked) {
|
|
39
|
+
labelPosition = idx == lastBarSeriesIdx ? 'top' : 'inside'
|
|
40
|
+
}
|
|
41
|
+
if (s.type == 'bar' && swapXY) {
|
|
42
|
+
labelPosition = 'right'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const standardSeriesOptions = {
|
|
46
|
+
type: s.type,
|
|
47
|
+
name: s.name,
|
|
48
|
+
data: data.map((row: any) => {
|
|
49
|
+
let x, y
|
|
50
|
+
if (swapXY) {
|
|
51
|
+
x = row[s.name]
|
|
52
|
+
y = row[config.xAxis.key]
|
|
53
|
+
} else {
|
|
54
|
+
x = row[config.xAxis.key]
|
|
55
|
+
y = row[s.name]
|
|
56
|
+
}
|
|
57
|
+
return [x, y]
|
|
58
|
+
}),
|
|
59
|
+
yAxisIndex: s.axis === 'y2' ? 1 : 0,
|
|
60
|
+
label: {
|
|
61
|
+
show: s.showDataLabels,
|
|
62
|
+
position: labelPosition,
|
|
63
|
+
formatter: (params: any) => {
|
|
64
|
+
const _val = swapXY ? params.value?.[0] : params.value?.[1]
|
|
65
|
+
return formatValue(_val, 1, true)
|
|
74
66
|
},
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
67
|
+
fontSize: 11,
|
|
68
|
+
},
|
|
69
|
+
labelLayout: { hideOverlap: true },
|
|
70
|
+
itemStyle: {
|
|
71
|
+
color: s.color,
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let seriesTypeOptions = {}
|
|
76
|
+
if (s.type === 'bar') {
|
|
77
|
+
seriesTypeOptions = getBarSeriesOptions(config, s)
|
|
78
|
+
}
|
|
79
|
+
if (s.type === 'line') {
|
|
80
|
+
seriesTypeOptions = getLineSeriesOptions(config, s)
|
|
81
|
+
}
|
|
82
|
+
if (s.type === 'area') {
|
|
83
|
+
seriesTypeOptions = getAreaSeriesOptions(config, s)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return mergeDeep(standardSeriesOptions, seriesTypeOptions, s.echartOptions)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
return mergeDeep(baseOptions, config.echartOptions)
|
|
91
90
|
}
|
|
92
91
|
|
|
93
92
|
function getBarSeriesOptions(config: AxisChartConfig, series: BarSeriesConfig) {
|
|
@@ -143,22 +142,3 @@ function getAreaSeriesOptions(
|
|
|
143
142
|
},
|
|
144
143
|
}
|
|
145
144
|
}
|
|
146
|
-
|
|
147
|
-
function isObject(item: any) {
|
|
148
|
-
return item && typeof item === 'object' && !Array.isArray(item)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function mergeDeep(target: any, source: any) {
|
|
152
|
-
let output = Object.assign({}, target)
|
|
153
|
-
if (isObject(target) && isObject(source)) {
|
|
154
|
-
Object.keys(source).forEach((key) => {
|
|
155
|
-
if (isObject(source[key])) {
|
|
156
|
-
if (!(key in target)) Object.assign(output, { [key]: source[key] })
|
|
157
|
-
else output[key] = mergeDeep(target[key], source[key])
|
|
158
|
-
} else {
|
|
159
|
-
Object.assign(output, { [key]: source[key] })
|
|
160
|
-
}
|
|
161
|
-
})
|
|
162
|
-
}
|
|
163
|
-
return output
|
|
164
|
-
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatDate, formatLabel, formatValue } from './helpers'
|
|
1
|
+
import { formatDate, formatLabel, formatValue, mergeDeep } from './helpers'
|
|
2
2
|
import { AxisChartConfig } from './types'
|
|
3
3
|
|
|
4
4
|
export const PADDING_TOP = 0
|
|
@@ -154,7 +154,7 @@ export function getTitleOptions(title: string, subtitle?: string) {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
function getXAxisOptions(config: AxisChartConfig) {
|
|
157
|
-
|
|
157
|
+
const options = config.swapXY
|
|
158
158
|
? {
|
|
159
159
|
show: true,
|
|
160
160
|
type: 'value',
|
|
@@ -220,10 +220,12 @@ function getXAxisOptions(config: AxisChartConfig) {
|
|
|
220
220
|
margin: 8,
|
|
221
221
|
},
|
|
222
222
|
}
|
|
223
|
+
|
|
224
|
+
return mergeDeep(options, config.swapXY ? config.yAxis.echartOptions : config.xAxis.echartOptions)
|
|
223
225
|
}
|
|
224
226
|
|
|
225
227
|
function getYAxisOptions(config: AxisChartConfig) {
|
|
226
|
-
|
|
228
|
+
let primaryYAxisOptions = config.swapXY
|
|
227
229
|
? {
|
|
228
230
|
show: true,
|
|
229
231
|
type: config.xAxis.type,
|
|
@@ -288,7 +290,12 @@ function getYAxisOptions(config: AxisChartConfig) {
|
|
|
288
290
|
max: config.yAxis.yMax,
|
|
289
291
|
}
|
|
290
292
|
|
|
291
|
-
|
|
293
|
+
primaryYAxisOptions = mergeDeep(
|
|
294
|
+
primaryYAxisOptions,
|
|
295
|
+
config.swapXY ? config.xAxis.echartOptions : config.yAxis.echartOptions
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
let secondaryYAxisOptions = {
|
|
292
299
|
show: false,
|
|
293
300
|
type: 'value',
|
|
294
301
|
z: 2,
|
|
@@ -330,6 +337,11 @@ function getYAxisOptions(config: AxisChartConfig) {
|
|
|
330
337
|
max: config.y2Axis?.yMax,
|
|
331
338
|
}
|
|
332
339
|
|
|
340
|
+
secondaryYAxisOptions = mergeDeep(
|
|
341
|
+
secondaryYAxisOptions,
|
|
342
|
+
config.swapXY ? config.y2Axis?.echartOptions : config.y2Axis?.echartOptions
|
|
343
|
+
)
|
|
344
|
+
|
|
333
345
|
return config.swapXY
|
|
334
346
|
? [primaryYAxisOptions]
|
|
335
347
|
: [primaryYAxisOptions, secondaryYAxisOptions]
|
|
@@ -73,3 +73,34 @@ export function formatDate(date: string, format?: string, grain: TimeGrain = 'da
|
|
|
73
73
|
|
|
74
74
|
return dayjs(date).format(format)
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
export function isObject(item: any) {
|
|
79
|
+
return item && typeof item === 'object' && !Array.isArray(item)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function mergeDeep(target: any, ...sources: any[]) {
|
|
83
|
+
if (!sources.length) return target;
|
|
84
|
+
const source = sources.shift();
|
|
85
|
+
|
|
86
|
+
if (!source || !isObject(target) || !isObject(source)) {
|
|
87
|
+
// Skip the current source if it's not a proper object
|
|
88
|
+
return mergeDeep(target, ...sources);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let output = Object.assign({}, target);
|
|
92
|
+
|
|
93
|
+
Object.keys(source).forEach((key) => {
|
|
94
|
+
if (isObject(source[key])) {
|
|
95
|
+
if (!(key in output)) {
|
|
96
|
+
output[key] = source[key];
|
|
97
|
+
} else {
|
|
98
|
+
output[key] = mergeDeep(output[key], source[key]);
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
output[key] = source[key];
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return mergeDeep(output, ...sources);
|
|
106
|
+
}
|
|
@@ -10,20 +10,32 @@ export type AxisChartConfig = {
|
|
|
10
10
|
type: 'category' | 'time' | 'value'
|
|
11
11
|
timeGrain?: TimeGrain
|
|
12
12
|
title?: string
|
|
13
|
+
echartOptions?: {
|
|
14
|
+
[key: string]: any
|
|
15
|
+
}
|
|
13
16
|
}
|
|
14
17
|
yAxis: {
|
|
15
18
|
title?: string
|
|
16
19
|
yMin?: number
|
|
17
20
|
yMax?: number
|
|
21
|
+
echartOptions?: {
|
|
22
|
+
[key: string]: any
|
|
23
|
+
}
|
|
18
24
|
}
|
|
19
25
|
y2Axis?: {
|
|
20
26
|
title?: string
|
|
21
27
|
yMin?: number
|
|
22
28
|
yMax?: number
|
|
29
|
+
echartOptions?: {
|
|
30
|
+
[key: string]: any
|
|
31
|
+
}
|
|
23
32
|
}
|
|
24
33
|
swapXY?: boolean
|
|
25
34
|
stacked?: boolean
|
|
26
35
|
series: (BarSeriesConfig | LineSeriesConfig | AreaSeriesConfig)[]
|
|
36
|
+
echartOptions?: {
|
|
37
|
+
[key: string]: any
|
|
38
|
+
}
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
export type SeriesConfig = {
|
|
@@ -32,6 +44,9 @@ export type SeriesConfig = {
|
|
|
32
44
|
color?: string
|
|
33
45
|
axis?: 'y' | 'y2'
|
|
34
46
|
showDataLabels?: boolean
|
|
47
|
+
echartOptions?: {
|
|
48
|
+
[key: string]: any
|
|
49
|
+
}
|
|
35
50
|
}
|
|
36
51
|
|
|
37
52
|
export type BarSeriesConfig = SeriesConfig & {
|
|
@@ -61,6 +76,9 @@ export type DonutChartConfig = {
|
|
|
61
76
|
valueColumn: string
|
|
62
77
|
maxSliceCount?: number
|
|
63
78
|
showInlineLabels?: boolean
|
|
79
|
+
echartOptions?: {
|
|
80
|
+
[key: string]: any
|
|
81
|
+
}
|
|
64
82
|
}
|
|
65
83
|
|
|
66
84
|
export type FunnelChartConfig = {
|
|
@@ -71,6 +89,9 @@ export type FunnelChartConfig = {
|
|
|
71
89
|
categoryColumn: string
|
|
72
90
|
valueColumn: string
|
|
73
91
|
showPercentages?: boolean
|
|
92
|
+
echartOptions?: {
|
|
93
|
+
[key: string]: any
|
|
94
|
+
}
|
|
74
95
|
}
|
|
75
96
|
|
|
76
97
|
export type NumberChartConfig = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div v-if="type != 'checkbox'" :class="['space-y-1.5', attrs.class]">
|
|
2
|
+
<div v-if="type != 'checkbox'" :class="['space-y-1.5', attrs.class]" :style="attrs.style">
|
|
3
3
|
<FormLabel
|
|
4
4
|
v-if="label"
|
|
5
5
|
:label="label"
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
v-if="type === 'select'"
|
|
12
12
|
:id="id"
|
|
13
13
|
v-bind="{ ...controlAttrs, size }"
|
|
14
|
+
v-model="model"
|
|
14
15
|
>
|
|
15
16
|
<template #prefix v-if="$slots.prefix">
|
|
16
17
|
<slot name="prefix" />
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
<Autocomplete
|
|
20
21
|
v-else-if="type === 'autocomplete'"
|
|
21
22
|
v-bind="{ ...controlAttrs }"
|
|
23
|
+
v-model="model"
|
|
22
24
|
>
|
|
23
25
|
<template #prefix v-if="$slots.prefix">
|
|
24
26
|
<slot name="prefix" />
|
|
@@ -31,11 +33,13 @@
|
|
|
31
33
|
v-else-if="type === 'textarea'"
|
|
32
34
|
:id="id"
|
|
33
35
|
v-bind="{ ...controlAttrs, size }"
|
|
36
|
+
v-model="model"
|
|
34
37
|
/>
|
|
35
38
|
<TextInput
|
|
36
39
|
v-else
|
|
37
40
|
:id="id"
|
|
38
41
|
v-bind="{ ...controlAttrs, type, size, required }"
|
|
42
|
+
v-model="model"
|
|
39
43
|
>
|
|
40
44
|
<template #prefix v-if="$slots.prefix">
|
|
41
45
|
<slot name="prefix" />
|
|
@@ -52,6 +56,7 @@
|
|
|
52
56
|
v-else
|
|
53
57
|
:id="id"
|
|
54
58
|
v-bind="{ ...controlAttrs, label, size, class: attrs.class }"
|
|
59
|
+
v-model="model"
|
|
55
60
|
/>
|
|
56
61
|
</template>
|
|
57
62
|
<script setup lang="ts">
|
|
@@ -79,6 +84,8 @@ const props = withDefaults(defineProps<FormControlProps>(), {
|
|
|
79
84
|
size: 'sm',
|
|
80
85
|
})
|
|
81
86
|
|
|
87
|
+
const model = defineModel()
|
|
88
|
+
|
|
82
89
|
const attrs = useAttrs()
|
|
83
90
|
const controlAttrs = computed(() => {
|
|
84
91
|
// pass everything except class and style
|
|
@@ -91,16 +98,6 @@ const controlAttrs = computed(() => {
|
|
|
91
98
|
return _attrs
|
|
92
99
|
})
|
|
93
100
|
|
|
94
|
-
const labelClasses = computed(() => {
|
|
95
|
-
return [
|
|
96
|
-
{
|
|
97
|
-
sm: 'text-xs',
|
|
98
|
-
md: 'text-base',
|
|
99
|
-
}[props.size],
|
|
100
|
-
'text-ink-gray-5',
|
|
101
|
-
]
|
|
102
|
-
})
|
|
103
|
-
|
|
104
101
|
const descriptionClasses = computed(() => {
|
|
105
102
|
return [
|
|
106
103
|
{
|
package/src/components/Input.vue
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<label :class="[type == 'checkbox' ? 'flex' : 'block', $attrs.class]">
|
|
2
|
+
<label :class="[type == 'checkbox' ? 'flex' : 'block', $attrs.class]" :style="$attrs.style">
|
|
3
3
|
<span
|
|
4
4
|
v-if="label && type != 'checkbox'"
|
|
5
5
|
class="mb-2 block text-sm leading-4 text-gray-700"
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div
|
|
3
|
+
class="relative w-full"
|
|
4
|
+
:class="$attrs.class"
|
|
5
|
+
:style="$attrs.style"
|
|
6
|
+
v-if="editor"
|
|
7
|
+
>
|
|
3
8
|
<TextEditorBubbleMenu :buttons="bubbleMenu" :options="bubbleMenuOptions" />
|
|
4
9
|
<TextEditorFixedMenu
|
|
5
10
|
class="w-full overflow-x-auto rounded-t-lg border border-outline-gray-modals"
|
|
@@ -14,7 +19,7 @@
|
|
|
14
19
|
</div>
|
|
15
20
|
</template>
|
|
16
21
|
|
|
17
|
-
<script>
|
|
22
|
+
<script lang="ts">
|
|
18
23
|
import { normalizeClass, computed } from 'vue'
|
|
19
24
|
import { Editor, EditorContent, VueNodeViewRenderer } from '@tiptap/vue-3'
|
|
20
25
|
import StarterKit from '@tiptap/starter-kit'
|
|
@@ -39,10 +44,12 @@ import configureMention from './mention'
|
|
|
39
44
|
import TextEditorFixedMenu from './TextEditorFixedMenu.vue'
|
|
40
45
|
import TextEditorBubbleMenu from './TextEditorBubbleMenu.vue'
|
|
41
46
|
import TextEditorFloatingMenu from './TextEditorFloatingMenu.vue'
|
|
42
|
-
import EmojiExtension from './emoji-extension'
|
|
43
|
-
import SlashCommands from './slash-commands-extension'
|
|
47
|
+
import EmojiExtension from './extensions/emoji/emoji-extension'
|
|
48
|
+
import SlashCommands from './extensions/slash-commands/slash-commands-extension'
|
|
44
49
|
import { detectMarkdown, markdownToHTML } from '../../utils/markdown'
|
|
45
50
|
import { DOMParser } from 'prosemirror-model'
|
|
51
|
+
import { TagNode, TagExtension } from './extensions/tag/tag-extension'
|
|
52
|
+
import { Heading } from './extensions/heading/heading'
|
|
46
53
|
|
|
47
54
|
const lowlight = createLowlight(common)
|
|
48
55
|
|
|
@@ -100,6 +107,10 @@ export default {
|
|
|
100
107
|
type: Array,
|
|
101
108
|
default: () => [],
|
|
102
109
|
},
|
|
110
|
+
tags: {
|
|
111
|
+
type: Array,
|
|
112
|
+
default: () => [],
|
|
113
|
+
},
|
|
103
114
|
uploadFunction: {
|
|
104
115
|
type: Function,
|
|
105
116
|
default: () => null,
|
|
@@ -147,6 +158,13 @@ export default {
|
|
|
147
158
|
StarterKit.configure({
|
|
148
159
|
...this.starterkitOptions,
|
|
149
160
|
codeBlock: false,
|
|
161
|
+
heading: false,
|
|
162
|
+
}),
|
|
163
|
+
Heading.configure({
|
|
164
|
+
...(typeof this.starterkitOptions?.heading === 'object' &&
|
|
165
|
+
this.starterkitOptions.heading !== null
|
|
166
|
+
? this.starterkitOptions.heading
|
|
167
|
+
: {}),
|
|
150
168
|
}),
|
|
151
169
|
Table.configure({
|
|
152
170
|
resizable: true,
|
|
@@ -185,6 +203,10 @@ export default {
|
|
|
185
203
|
configureMention(this.mentions),
|
|
186
204
|
EmojiExtension,
|
|
187
205
|
SlashCommands,
|
|
206
|
+
TagNode,
|
|
207
|
+
TagExtension.configure({
|
|
208
|
+
tags: () => this.tags,
|
|
209
|
+
}),
|
|
188
210
|
...(this.extensions || []),
|
|
189
211
|
],
|
|
190
212
|
onUpdate: ({ editor }) => {
|
|
@@ -306,4 +328,23 @@ img.ProseMirror-selectednode {
|
|
|
306
328
|
border-radius: 3px;
|
|
307
329
|
padding: 0 2px;
|
|
308
330
|
}
|
|
331
|
+
.tag-item,
|
|
332
|
+
.tag-suggestion-active {
|
|
333
|
+
background-color: var(--surface-gray-1, #f8f8f8);
|
|
334
|
+
color: inherit;
|
|
335
|
+
border: 1px solid transparent;
|
|
336
|
+
padding: 0px 2px;
|
|
337
|
+
border-radius: 4px;
|
|
338
|
+
font-size: 1em;
|
|
339
|
+
white-space: nowrap;
|
|
340
|
+
cursor: default;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.tag-item.ProseMirror-selectednode {
|
|
344
|
+
border-color: var(--outline-gray-3, #c7c7c7);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.tag-suggestion-active {
|
|
348
|
+
background-color: var(--surface-gray-2, #f3f3f3);
|
|
349
|
+
}
|
|
309
350
|
</style>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<SuggestionList
|
|
3
3
|
ref="suggestionList"
|
|
4
4
|
:items="items"
|
|
5
|
-
:command="
|
|
5
|
+
:command="(item) => onItemSelect(item as EmojiItem)"
|
|
6
6
|
item-class="py-2"
|
|
7
7
|
>
|
|
8
8
|
<template #default="{ item }">
|
|
@@ -14,29 +14,34 @@
|
|
|
14
14
|
|
|
15
15
|
<script setup lang="ts">
|
|
16
16
|
import { ref, type PropType } from 'vue'
|
|
17
|
-
import SuggestionList
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
name: string
|
|
21
|
-
emoji: string
|
|
22
|
-
}
|
|
17
|
+
import SuggestionList from '../suggestion/SuggestionList.vue'
|
|
18
|
+
import type { Editor, Range } from '@tiptap/core'
|
|
19
|
+
import type { EmojiItem } from './emoji-extension'
|
|
23
20
|
|
|
24
21
|
const props = defineProps({
|
|
25
22
|
items: {
|
|
26
23
|
type: Array as PropType<EmojiItem[]>,
|
|
27
24
|
required: true,
|
|
28
25
|
},
|
|
26
|
+
editor: {
|
|
27
|
+
type: Object as PropType<Editor>,
|
|
28
|
+
required: true,
|
|
29
|
+
},
|
|
30
|
+
range: {
|
|
31
|
+
type: Object as PropType<Range>,
|
|
32
|
+
required: true,
|
|
33
|
+
},
|
|
29
34
|
command: {
|
|
30
|
-
type: Function as PropType<(
|
|
35
|
+
type: Function as PropType<(item: EmojiItem) => void>,
|
|
31
36
|
required: true,
|
|
32
37
|
},
|
|
33
38
|
})
|
|
34
39
|
|
|
35
40
|
const suggestionList = ref<InstanceType<typeof SuggestionList> | null>(null)
|
|
36
41
|
|
|
37
|
-
const
|
|
42
|
+
const onItemSelect = (item: EmojiItem) => {
|
|
38
43
|
if (item) {
|
|
39
|
-
props.command(
|
|
44
|
+
props.command(item)
|
|
40
45
|
}
|
|
41
46
|
}
|
|
42
47
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { PluginKey } from 'prosemirror-state'
|
|
2
|
+
import {
|
|
3
|
+
BaseSuggestionItem,
|
|
4
|
+
createSuggestionExtension,
|
|
5
|
+
} from '../suggestion/createSuggestionExtension'
|
|
6
|
+
import EmojiList from './EmojiList.vue'
|
|
7
|
+
import _EMOJIS from './emojis.json'
|
|
8
|
+
|
|
9
|
+
const EMOJIS = _EMOJIS as EmojiItem[]
|
|
10
|
+
|
|
11
|
+
export interface EmojiItem extends BaseSuggestionItem {
|
|
12
|
+
name: string
|
|
13
|
+
emoji: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default createSuggestionExtension<EmojiItem>({
|
|
17
|
+
name: 'emoji',
|
|
18
|
+
char: ':',
|
|
19
|
+
pluginKey: new PluginKey('emojiSuggestion'),
|
|
20
|
+
items: ({ query }: { query: string }) => {
|
|
21
|
+
return EMOJIS.filter((item) =>
|
|
22
|
+
item.name.toLowerCase().includes(query.toLowerCase()),
|
|
23
|
+
)
|
|
24
|
+
.sort((a, b) => {
|
|
25
|
+
const aName = a.name.toLowerCase()
|
|
26
|
+
const bName = b.name.toLowerCase()
|
|
27
|
+
const queryLower = query.toLowerCase()
|
|
28
|
+
|
|
29
|
+
// Exact matches first
|
|
30
|
+
if (aName === queryLower && bName !== queryLower) return -1
|
|
31
|
+
if (bName === queryLower && aName !== queryLower) return 1
|
|
32
|
+
|
|
33
|
+
// Then names starting with the query
|
|
34
|
+
if (aName.startsWith(queryLower) && !bName.startsWith(queryLower))
|
|
35
|
+
return -1
|
|
36
|
+
if (bName.startsWith(queryLower) && !aName.startsWith(queryLower))
|
|
37
|
+
return 1
|
|
38
|
+
|
|
39
|
+
// Then sort by name length (shorter first)
|
|
40
|
+
return aName.length - bName.length
|
|
41
|
+
})
|
|
42
|
+
.slice(0, 5)
|
|
43
|
+
},
|
|
44
|
+
command: ({ editor, range, props: item }) => {
|
|
45
|
+
if (item && item.emoji) {
|
|
46
|
+
editor.chain().focus().deleteRange(range).insertContent(item.emoji).run()
|
|
47
|
+
} else {
|
|
48
|
+
console.error(
|
|
49
|
+
'Emoji command execution error: emoji property not found on selected item or item is invalid.',
|
|
50
|
+
item,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
component: EmojiList,
|
|
55
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import TiptapHeading from '@tiptap/extension-heading'
|
|
2
|
+
import { textblockTypeInputRule } from '@tiptap/core'
|
|
3
|
+
|
|
4
|
+
// This custom Heading extension modifies the default input rule behavior.
|
|
5
|
+
// The default Tiptap heading input rule converts text to a heading when '#' is followed by any whitespace (including Enter).
|
|
6
|
+
// This customization ensures that headings are only created when '#' is followed by a literal space.
|
|
7
|
+
// This change is necessary to prevent conflicts with other extensions, such as a tags extension,
|
|
8
|
+
// which uses '#' followed by Enter to add a tag.
|
|
9
|
+
export const Heading = TiptapHeading.extend({
|
|
10
|
+
addInputRules() {
|
|
11
|
+
return this.options.levels.map((level) => {
|
|
12
|
+
let regexp = new RegExp(`^(#{${level}}) $`)
|
|
13
|
+
return textblockTypeInputRule({
|
|
14
|
+
find: regexp,
|
|
15
|
+
type: this.type,
|
|
16
|
+
getAttributes: { level },
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
},
|
|
20
|
+
})
|