pimelon-ui 0.1.205 → 0.1.208
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/ListView/ListRow.vue +29 -7
- package/src/components/TextEditor/TextEditor.vue +30 -12
- package/src/components/TextEditor/extensions/code-block.ts +118 -0
- package/src/components/TextEditor/extensions/content-paste-extension.ts +79 -9
- package/src/components/TextEditor/extensions/heading/heading.ts +1 -1
- package/src/components/TextEditor/extensions/image/image-extension.ts +11 -7
- package/src/components/TextEditor/extensions/mention/mention-extension.ts +3 -5
- package/src/components/TextEditor/extensions/mention/style.css +5 -1
- package/src/components/TextEditor/extensions/tag/tag-extension.ts +4 -2
- package/src/components/TextEditor/link-extension.ts +0 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pimelon-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.208",
|
|
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
|
"@tailwindcss/line-clamp": "^0.4.4",
|
|
38
38
|
"@tailwindcss/typography": "^0.5.16",
|
|
39
39
|
"@tiptap/core": "^2.26.1",
|
|
40
|
+
"@tiptap/extension-code": "^2.26.1",
|
|
40
41
|
"@tiptap/extension-code-block": "^2.26.1",
|
|
41
42
|
"@tiptap/extension-code-block-lowlight": "^2.26.1",
|
|
42
43
|
"@tiptap/extension-color": "^2.26.1",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
class="flex flex-col transition-all duration-300 ease-in-out"
|
|
15
15
|
v-bind="{
|
|
16
16
|
to: list.options.getRowRoute ? list.options.getRowRoute(row) : undefined,
|
|
17
|
-
onClick:
|
|
17
|
+
onClick: onRowClick,
|
|
18
18
|
}"
|
|
19
19
|
>
|
|
20
20
|
<component
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
<Checkbox
|
|
41
41
|
:modelValue="isSelected"
|
|
42
42
|
class="cursor-pointer duration-300"
|
|
43
|
-
@click.stop="
|
|
43
|
+
@click.stop="handleCheckboxClick"
|
|
44
44
|
/>
|
|
45
45
|
</div>
|
|
46
46
|
<div
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
import Checkbox from '../Checkbox/Checkbox.vue'
|
|
90
90
|
import ListRowItem from './ListRowItem.vue'
|
|
91
91
|
import { alignmentMap, getGridTemplateColumns } from './utils'
|
|
92
|
-
import { computed, inject
|
|
92
|
+
import { computed, inject } from 'vue'
|
|
93
93
|
|
|
94
94
|
const props = defineProps({
|
|
95
95
|
row: {
|
|
@@ -144,12 +144,34 @@ const roundedClass = computed(() => {
|
|
|
144
144
|
}
|
|
145
145
|
})
|
|
146
146
|
|
|
147
|
-
const onRowClick = (
|
|
148
|
-
if (list.value.options.onRowClick)
|
|
149
|
-
|
|
147
|
+
const onRowClick = (event) => {
|
|
148
|
+
if (list.value.options.onRowClick)
|
|
149
|
+
list.value.options.onRowClick(props.row, event)
|
|
150
|
+
if (list.value.activeRow.value === props.row.name) {
|
|
150
151
|
list.value.activeRow.value = null
|
|
151
152
|
} else {
|
|
152
|
-
list.value.activeRow.value = row.name
|
|
153
|
+
list.value.activeRow.value = props.row.name
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const handleCheckboxClick = (event) => {
|
|
158
|
+
const value = props.row[list.value.rowKey]
|
|
159
|
+
if (event.shiftKey && !list.value.selections.has(value)) {
|
|
160
|
+
const lastSelected = Array.from(list.value.selections).pop()
|
|
161
|
+
const rows = list.value.rows.find((k) => k.group)
|
|
162
|
+
? list.value.rows.reduce((acc, curr) => acc.concat(curr.rows), [])
|
|
163
|
+
: list.value.rows
|
|
164
|
+
const lastIndex = rows.findIndex(
|
|
165
|
+
(k) => lastSelected === k[list.value.rowKey],
|
|
166
|
+
)
|
|
167
|
+
const curIndex = rows.findIndex((k) => value === k[list.value.rowKey])
|
|
168
|
+
const start = Math.min(lastIndex, curIndex)
|
|
169
|
+
const end = Math.max(lastIndex, curIndex)
|
|
170
|
+
for (let i = start; i <= end; i++) {
|
|
171
|
+
list.value.selections.add(rows[i][list.value.rowKey])
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
list.value.toggleRow(value)
|
|
153
175
|
}
|
|
154
176
|
}
|
|
155
177
|
</script>
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
class="relative w-full"
|
|
5
5
|
:class="attrsClass"
|
|
6
6
|
:style="attrsStyle"
|
|
7
|
+
v-bind="attrsWithoutClassStyle"
|
|
8
|
+
ref="rootRef"
|
|
7
9
|
>
|
|
8
10
|
<TextEditorBubbleMenu :buttons="bubbleMenu" :options="bubbleMenuOptions" />
|
|
9
11
|
<TextEditorFixedMenu
|
|
@@ -30,11 +32,12 @@ import {
|
|
|
30
32
|
provide,
|
|
31
33
|
ref,
|
|
32
34
|
useAttrs,
|
|
35
|
+
useTemplateRef,
|
|
33
36
|
} from 'vue'
|
|
34
37
|
|
|
35
38
|
defineOptions({ inheritAttrs: false })
|
|
36
39
|
|
|
37
|
-
import { Editor, EditorContent
|
|
40
|
+
import { Editor, EditorContent } from '@tiptap/vue-3'
|
|
38
41
|
import StarterKit from '@tiptap/starter-kit'
|
|
39
42
|
import Placeholder from '@tiptap/extension-placeholder'
|
|
40
43
|
import TextAlign from '@tiptap/extension-text-align'
|
|
@@ -53,9 +56,7 @@ import TaskItem from '@tiptap/extension-task-item'
|
|
|
53
56
|
import TaskList from '@tiptap/extension-task-list'
|
|
54
57
|
import NamedColorExtension from './extensions/color'
|
|
55
58
|
import NamedHighlightExtension from './extensions/highlight'
|
|
56
|
-
|
|
57
|
-
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
|
|
58
|
-
import CodeBlockComponent from './CodeBlockComponent.vue'
|
|
59
|
+
|
|
59
60
|
import { MentionExtension } from './extensions/mention'
|
|
60
61
|
import TextEditorFixedMenu from './TextEditorFixedMenu.vue'
|
|
61
62
|
import TextEditorBubbleMenu from './TextEditorBubbleMenu.vue'
|
|
@@ -66,11 +67,10 @@ import { ContentPasteExtension } from './extensions/content-paste-extension'
|
|
|
66
67
|
import { TagNode, TagExtension } from './extensions/tag/tag-extension'
|
|
67
68
|
import { Heading } from './extensions/heading/heading'
|
|
68
69
|
import { ImageGroup } from './extensions/image-group/image-group-extension'
|
|
70
|
+
import { ExtendedCode, ExtendedCodeBlock } from './extensions/code-block'
|
|
69
71
|
import { useFileUpload } from '../../utils/useFileUpload'
|
|
70
72
|
import { TextEditorEmits, TextEditorProps } from './types'
|
|
71
73
|
|
|
72
|
-
const lowlight = createLowlight(common)
|
|
73
|
-
|
|
74
74
|
function defaultUploadFunction(file: File) {
|
|
75
75
|
// useFileUpload is melon specific
|
|
76
76
|
let fileUpload = useFileUpload()
|
|
@@ -100,6 +100,11 @@ const editor = ref<Editor | null>(null)
|
|
|
100
100
|
const attrs = useAttrs()
|
|
101
101
|
const attrsClass = computed(() => normalizeClass(attrs.class))
|
|
102
102
|
const attrsStyle = computed(() => normalizeStyle(attrs.style))
|
|
103
|
+
const attrsWithoutClassStyle = computed(() => {
|
|
104
|
+
return Object.fromEntries(
|
|
105
|
+
Object.entries(attrs).filter(([key]) => key !== 'class' && key !== 'style'),
|
|
106
|
+
)
|
|
107
|
+
})
|
|
103
108
|
|
|
104
109
|
const editorProps = computed(() => {
|
|
105
110
|
return {
|
|
@@ -154,8 +159,23 @@ onMounted(() => {
|
|
|
154
159
|
extensions: [
|
|
155
160
|
StarterKit.configure({
|
|
156
161
|
...props.starterkitOptions,
|
|
162
|
+
code: false,
|
|
157
163
|
codeBlock: false,
|
|
158
164
|
heading: false,
|
|
165
|
+
}).extend({
|
|
166
|
+
addKeyboardShortcuts() {
|
|
167
|
+
return {
|
|
168
|
+
Backspace: () => {
|
|
169
|
+
const { $from } = this.editor.view.state.selection
|
|
170
|
+
if (
|
|
171
|
+
!this.editor.can().liftListItem('listItem') ||
|
|
172
|
+
$from.parentOffset > 0
|
|
173
|
+
)
|
|
174
|
+
return false
|
|
175
|
+
return this.editor.commands.liftListItem('listItem')
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
},
|
|
159
179
|
}),
|
|
160
180
|
Heading.configure({
|
|
161
181
|
...(typeof props.starterkitOptions?.heading === 'object' &&
|
|
@@ -180,11 +200,8 @@ onMounted(() => {
|
|
|
180
200
|
TextStyle,
|
|
181
201
|
NamedColorExtension,
|
|
182
202
|
NamedHighlightExtension,
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return VueNodeViewRenderer(CodeBlockComponent)
|
|
186
|
-
},
|
|
187
|
-
}).configure({ lowlight }),
|
|
203
|
+
ExtendedCode,
|
|
204
|
+
ExtendedCodeBlock,
|
|
188
205
|
ImageExtension.configure({
|
|
189
206
|
uploadFunction: props.uploadFunction || defaultUploadFunction,
|
|
190
207
|
}),
|
|
@@ -222,7 +239,6 @@ onMounted(() => {
|
|
|
222
239
|
}),
|
|
223
240
|
ContentPasteExtension.configure({
|
|
224
241
|
enabled: true,
|
|
225
|
-
showConfirmation: true,
|
|
226
242
|
uploadFunction: props.uploadFunction || defaultUploadFunction,
|
|
227
243
|
}),
|
|
228
244
|
...(props.extensions || []),
|
|
@@ -254,8 +270,10 @@ provide(
|
|
|
254
270
|
computed(() => editor.value),
|
|
255
271
|
)
|
|
256
272
|
|
|
273
|
+
const rootRef = useTemplateRef('rootRef')
|
|
257
274
|
defineExpose({
|
|
258
275
|
editor,
|
|
276
|
+
rootRef,
|
|
259
277
|
})
|
|
260
278
|
</script>
|
|
261
279
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { common, createLowlight } from 'lowlight'
|
|
2
|
+
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
|
|
3
|
+
import Code from '@tiptap/extension-code'
|
|
4
|
+
import CodeBlockComponent from '../CodeBlockComponent.vue'
|
|
5
|
+
import { VueNodeViewRenderer } from '@tiptap/vue-3'
|
|
6
|
+
import { markInputRule } from '@tiptap/core'
|
|
7
|
+
|
|
8
|
+
const INDENT = ' '.repeat(4)
|
|
9
|
+
export const inputRegex = /(?<=^|[^`])`([^`]+)`(?!`)$/
|
|
10
|
+
|
|
11
|
+
function getCodeBlockCtx(state: any) {
|
|
12
|
+
const { $from, from, to } = state.selection
|
|
13
|
+
let d = $from.depth
|
|
14
|
+
while (d > 0 && $from.node(d).type.name !== 'codeBlock') d--
|
|
15
|
+
if (d === 0) return null
|
|
16
|
+
|
|
17
|
+
const node = $from.node(d)
|
|
18
|
+
const start = $from.start(d)
|
|
19
|
+
const text: string = node.textContent
|
|
20
|
+
const fromOffset = from - start
|
|
21
|
+
const toOffset = to - start
|
|
22
|
+
return { start, text, fromOffset, toOffset }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function lineStartsBetween(text: string, fromOffset: number, toOffset: number) {
|
|
26
|
+
let startOff = text.lastIndexOf('\n', Math.max(0, fromOffset - 1)) + 1
|
|
27
|
+
let endOff = toOffset
|
|
28
|
+
if (endOff > 0 && text[endOff - 1] === '\n') endOff--
|
|
29
|
+
|
|
30
|
+
const starts: number[] = [startOff]
|
|
31
|
+
let i = startOff
|
|
32
|
+
while (true) {
|
|
33
|
+
const nl = text.indexOf('\n', i)
|
|
34
|
+
if (nl === -1 || nl >= endOff) break
|
|
35
|
+
starts.push(nl + 1)
|
|
36
|
+
i = nl + 1
|
|
37
|
+
}
|
|
38
|
+
return starts
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const lowlight = createLowlight(common)
|
|
42
|
+
|
|
43
|
+
export const ExtendedCodeBlock = CodeBlockLowlight.extend({
|
|
44
|
+
addNodeView() {
|
|
45
|
+
return VueNodeViewRenderer(CodeBlockComponent)
|
|
46
|
+
},
|
|
47
|
+
addKeyboardShortcuts() {
|
|
48
|
+
return {
|
|
49
|
+
Tab: () => {
|
|
50
|
+
const { state, dispatch } = this.editor.view
|
|
51
|
+
const ctx = getCodeBlockCtx(state)
|
|
52
|
+
if (!ctx) return false
|
|
53
|
+
|
|
54
|
+
const { start, text, fromOffset, toOffset } = ctx
|
|
55
|
+
const multiline = text.slice(fromOffset, toOffset).includes('\n')
|
|
56
|
+
const tr = state.tr
|
|
57
|
+
if (multiline) {
|
|
58
|
+
for (const off of lineStartsBetween(text, fromOffset, toOffset)) {
|
|
59
|
+
const pos = tr.mapping.map(start + off)
|
|
60
|
+
tr.insertText(INDENT, pos)
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
tr.insertText(INDENT, state.selection.from)
|
|
64
|
+
}
|
|
65
|
+
dispatch(tr)
|
|
66
|
+
return true
|
|
67
|
+
},
|
|
68
|
+
'Shift-Tab': () => {
|
|
69
|
+
const { state, dispatch } = this.editor.view
|
|
70
|
+
const ctx = getCodeBlockCtx(state)
|
|
71
|
+
if (!ctx) return false
|
|
72
|
+
|
|
73
|
+
const { start, text, fromOffset, toOffset } = ctx
|
|
74
|
+
const tr = state.tr
|
|
75
|
+
for (const off of lineStartsBetween(text, fromOffset, toOffset)) {
|
|
76
|
+
let len = 0
|
|
77
|
+
if (text.substr(off, 4) === INDENT) len = 4
|
|
78
|
+
else if (text[off] === '\t') len = 1
|
|
79
|
+
|
|
80
|
+
if (len > 0) {
|
|
81
|
+
const s = tr.mapping.map(start + off)
|
|
82
|
+
const e = tr.mapping.map(start + off + len)
|
|
83
|
+
tr.delete(s, e)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
dispatch(tr)
|
|
88
|
+
return true
|
|
89
|
+
},
|
|
90
|
+
'`': () => {
|
|
91
|
+
const { from, to } = this.editor.state.selection
|
|
92
|
+
if (from === to) return false
|
|
93
|
+
return this.editor.commands.toggleCode()
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
}).configure({ lowlight })
|
|
98
|
+
|
|
99
|
+
export const ExtendedCode = Code.extend({
|
|
100
|
+
addKeyboardShortcuts() {
|
|
101
|
+
return {
|
|
102
|
+
'`': () => {
|
|
103
|
+
const { from, to } = this.editor.state.selection
|
|
104
|
+
if (from === to) return false
|
|
105
|
+
return this.editor.commands.toggleCode()
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
addInputRules() {
|
|
111
|
+
return [
|
|
112
|
+
markInputRule({
|
|
113
|
+
find: inputRegex,
|
|
114
|
+
type: this.type,
|
|
115
|
+
}),
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
})
|
|
@@ -7,7 +7,6 @@ import { processMultipleImages } from './image/image-extension'
|
|
|
7
7
|
|
|
8
8
|
export interface ContentPasteOptions {
|
|
9
9
|
enabled: boolean
|
|
10
|
-
showConfirmation: boolean
|
|
11
10
|
uploadFunction: Function | null
|
|
12
11
|
}
|
|
13
12
|
|
|
@@ -17,7 +16,6 @@ export const ContentPasteExtension = Extension.create<ContentPasteOptions>({
|
|
|
17
16
|
addOptions() {
|
|
18
17
|
return {
|
|
19
18
|
enabled: true,
|
|
20
|
-
showConfirmation: true,
|
|
21
19
|
uploadFunction: null,
|
|
22
20
|
}
|
|
23
21
|
},
|
|
@@ -28,6 +26,30 @@ export const ContentPasteExtension = Extension.create<ContentPasteOptions>({
|
|
|
28
26
|
new Plugin({
|
|
29
27
|
key: new PluginKey('contentPaste'),
|
|
30
28
|
props: {
|
|
29
|
+
handleDOMEvents: {
|
|
30
|
+
copy: (view, event) => {
|
|
31
|
+
const selection = window.getSelection()
|
|
32
|
+
if (!selection) return false
|
|
33
|
+
|
|
34
|
+
const container = document.createElement('div')
|
|
35
|
+
for (let i = 0; i < selection.rangeCount; i++) container.appendChild(selection.getRangeAt(i).cloneContents())
|
|
36
|
+
|
|
37
|
+
// Update relative image srcs
|
|
38
|
+
const images = container.querySelectorAll('img')
|
|
39
|
+
images.forEach((img) => {
|
|
40
|
+
const src = img.getAttribute('src')
|
|
41
|
+
if (src && src.startsWith('/')) {
|
|
42
|
+
img.setAttribute('src', `${window.location.origin}${src}`)
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// Override clipboard HTML
|
|
47
|
+
event.clipboardData?.setData('text/html', container.innerHTML)
|
|
48
|
+
event.clipboardData?.setData('text/plain', selection.toString())
|
|
49
|
+
event.preventDefault()
|
|
50
|
+
return true
|
|
51
|
+
},
|
|
52
|
+
},
|
|
31
53
|
handlePaste: (
|
|
32
54
|
view: EditorView,
|
|
33
55
|
event: ClipboardEvent,
|
|
@@ -47,19 +69,19 @@ export const ContentPasteExtension = Extension.create<ContentPasteOptions>({
|
|
|
47
69
|
return true
|
|
48
70
|
}
|
|
49
71
|
|
|
72
|
+
// handle html with media
|
|
73
|
+
const htmlData = event.clipboardData?.getData('text/html')
|
|
74
|
+
if (htmlData) {
|
|
75
|
+
processHTMLImages(htmlData, view, this.options)
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
|
|
50
79
|
// handle markdown pasting
|
|
51
80
|
const text = event.clipboardData?.getData('text/plain')
|
|
52
81
|
if (!text) return false
|
|
53
82
|
|
|
54
83
|
if (!detectMarkdown(text)) return false
|
|
55
84
|
|
|
56
|
-
if (this.options.showConfirmation) {
|
|
57
|
-
const shouldConvert = confirm(
|
|
58
|
-
'Do you want to convert markdown content to HTML before pasting?',
|
|
59
|
-
)
|
|
60
|
-
if (!shouldConvert) return false
|
|
61
|
-
}
|
|
62
|
-
|
|
63
85
|
const htmlContent = markdownToHTML(text)
|
|
64
86
|
const tempDiv = document.createElement('div')
|
|
65
87
|
tempDiv.innerHTML = htmlContent
|
|
@@ -79,3 +101,51 @@ export const ContentPasteExtension = Extension.create<ContentPasteOptions>({
|
|
|
79
101
|
]
|
|
80
102
|
},
|
|
81
103
|
})
|
|
104
|
+
|
|
105
|
+
async function processHTMLImages(
|
|
106
|
+
html: string,
|
|
107
|
+
view: EditorView,
|
|
108
|
+
extensionOptions: ContentPasteOptions,
|
|
109
|
+
): Promise<undefined> {
|
|
110
|
+
const tempDiv = document.createElement('div')
|
|
111
|
+
tempDiv.innerHTML = html
|
|
112
|
+
const images = tempDiv.querySelectorAll('img')
|
|
113
|
+
|
|
114
|
+
const parser = DOMParser.fromSchema(view.state.schema)
|
|
115
|
+
const parsedSlice = parser.parseSlice(tempDiv, {
|
|
116
|
+
preserveWhitespace: true,
|
|
117
|
+
})
|
|
118
|
+
const tr = view.state.tr.replaceSelection(parsedSlice)
|
|
119
|
+
view.dispatch(tr)
|
|
120
|
+
|
|
121
|
+
const imageInfo: Array<{ src: string; pos: number }> = []
|
|
122
|
+
view.state.doc.descendants((node, pos) => {
|
|
123
|
+
for (let img of images) {
|
|
124
|
+
const src = img.getAttribute('src')
|
|
125
|
+
if (node.type.name === 'image' && node.attrs['src'] === src) {
|
|
126
|
+
imageInfo.push([src, pos])
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// Process each image
|
|
132
|
+
const imagePromises = Array.from(imageInfo).map(async ([src, pos]) => {
|
|
133
|
+
if (
|
|
134
|
+
src.startsWith('data:') ||
|
|
135
|
+
src.startsWith('blob:')
|
|
136
|
+
) {
|
|
137
|
+
try {
|
|
138
|
+
const response = await fetch(src)
|
|
139
|
+
const blob = await response.blob()
|
|
140
|
+
const file = new File([blob], 'pasted-data-image.png', {
|
|
141
|
+
type: blob.type,
|
|
142
|
+
})
|
|
143
|
+
processMultipleImages([file], view, pos, extensionOptions)
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error('Failed to process image:', error)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
await Promise.all(imagePromises)
|
|
151
|
+
}
|
|
@@ -9,7 +9,7 @@ import { textblockTypeInputRule } from '@tiptap/core'
|
|
|
9
9
|
export const Heading = TiptapHeading.extend({
|
|
10
10
|
addInputRules() {
|
|
11
11
|
return this.options.levels.map((level) => {
|
|
12
|
-
let regexp = new RegExp(`^(#{${level}}) $`)
|
|
12
|
+
let regexp = new RegExp(`^(#{${level}})( |\\u00A0)$`)
|
|
13
13
|
return textblockTypeInputRule({
|
|
14
14
|
find: regexp,
|
|
15
15
|
type: this.type,
|
|
@@ -373,14 +373,18 @@ function uploadImageBase(
|
|
|
373
373
|
})
|
|
374
374
|
|
|
375
375
|
const tr = view.state.tr
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
376
|
+
if (insertMode === 'replace') {
|
|
377
|
+
if (pos === null) tr.replaceSelectionWith(node)
|
|
378
|
+
else {
|
|
379
|
+
const nodeAtPos = view.state.doc.nodeAt(pos)
|
|
380
|
+
if (nodeAtPos) tr.replaceWith(pos, pos + nodeAtPos.nodeSize, node)
|
|
381
|
+
}
|
|
381
382
|
} else {
|
|
382
|
-
|
|
383
|
-
|
|
383
|
+
if (pos != null) tr.insert(pos, node)
|
|
384
|
+
else {
|
|
385
|
+
const insertPos = view.state.selection.from
|
|
386
|
+
tr.insert(insertPos, node)
|
|
387
|
+
}
|
|
384
388
|
}
|
|
385
389
|
|
|
386
390
|
view.dispatch(tr)
|
|
@@ -24,7 +24,6 @@ function createMentionNode(component?: Component) {
|
|
|
24
24
|
inline: true,
|
|
25
25
|
selectable: true,
|
|
26
26
|
atom: true,
|
|
27
|
-
|
|
28
27
|
addOptions() {
|
|
29
28
|
return {
|
|
30
29
|
component: undefined,
|
|
@@ -82,6 +81,9 @@ function createMentionNode(component?: Component) {
|
|
|
82
81
|
`@${HTMLAttributes['data-label'] || HTMLAttributes.id || ''}`,
|
|
83
82
|
]
|
|
84
83
|
},
|
|
84
|
+
renderText({ node }: any) {
|
|
85
|
+
return `@${node.attrs.label || node.attrs.id || ''}`
|
|
86
|
+
}
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
if (component) {
|
|
@@ -139,10 +141,6 @@ const MentionSuggestionExtension =
|
|
|
139
141
|
type: 'mention',
|
|
140
142
|
attrs: attributes,
|
|
141
143
|
},
|
|
142
|
-
{
|
|
143
|
-
type: 'text',
|
|
144
|
-
text: ' ',
|
|
145
|
-
},
|
|
146
144
|
])
|
|
147
145
|
.run()
|
|
148
146
|
},
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
.mention {
|
|
2
2
|
font-weight: 600;
|
|
3
3
|
box-decoration-break: clone;
|
|
4
|
-
padding: 1px
|
|
4
|
+
padding: 0 1px;
|
|
5
5
|
border-radius: 4px;
|
|
6
6
|
display: inline-block;
|
|
7
7
|
}
|
|
8
|
+
|
|
9
|
+
.mention.ProseMirror-selectednode, .mention:hover {
|
|
10
|
+
background-color: var(--surface-gray-1, #f8f8f8);
|
|
11
|
+
}
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
BaseSuggestionItem,
|
|
6
6
|
} from '../suggestion/createSuggestionExtension'
|
|
7
7
|
import SuggestionList from '../suggestion/SuggestionList.vue'
|
|
8
|
-
import { toValue
|
|
8
|
+
import { toValue } from 'vue'
|
|
9
9
|
|
|
10
10
|
export const TagNode = Node.create({
|
|
11
11
|
name: 'tagItem',
|
|
@@ -59,7 +59,9 @@ export const TagNode = Node.create({
|
|
|
59
59
|
`#${HTMLAttributes['data-tag-label']}`,
|
|
60
60
|
]
|
|
61
61
|
},
|
|
62
|
-
|
|
62
|
+
renderText({ node }: any) {
|
|
63
|
+
return `#${node.attrs.tagLabel || ''}`
|
|
64
|
+
},
|
|
63
65
|
addCommands() {
|
|
64
66
|
return {
|
|
65
67
|
setTag:
|
|
@@ -111,16 +111,6 @@ export const LinkExtension = Link.extend({
|
|
|
111
111
|
return true
|
|
112
112
|
})
|
|
113
113
|
|
|
114
|
-
const posAfterLink = selectionTo
|
|
115
|
-
const charAfter =
|
|
116
|
-
posAfterLink < doc.content.size
|
|
117
|
-
? doc.textBetween(posAfterLink, posAfterLink + 1)
|
|
118
|
-
: null
|
|
119
|
-
|
|
120
|
-
if (charAfter === null || charAfter !== ' ') {
|
|
121
|
-
chain = chain.insertContent(' ')
|
|
122
|
-
}
|
|
123
|
-
|
|
124
114
|
chain.run()
|
|
125
115
|
})
|
|
126
116
|
.catch(() => {})
|