@kvass/rich-text 1.5.1 → 1.5.3
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 +1 -1
- package/src/Component.vue +22 -16
- package/src/lines.js +152 -0
package/package.json
CHANGED
package/src/Component.vue
CHANGED
|
@@ -32,9 +32,7 @@
|
|
|
32
32
|
v-for="(item, index) in getCommands('bubble')"
|
|
33
33
|
:key="'bubble' + index"
|
|
34
34
|
class="richtext__bubble-button"
|
|
35
|
-
:class="{
|
|
36
|
-
'is-active': editor.isActive(item.isActiveKey || item.stateKey, item.commandProps),
|
|
37
|
-
}"
|
|
35
|
+
:class="{ 'is-active': isItemActive(item) }"
|
|
38
36
|
:disabled="disabled || item.disabled"
|
|
39
37
|
@click="() => onMenuBarButtonClick(item)"
|
|
40
38
|
type="button"
|
|
@@ -53,9 +51,7 @@
|
|
|
53
51
|
v-for="(item, index) in getCommands('menu')"
|
|
54
52
|
:key="'menubar' + index"
|
|
55
53
|
type="button"
|
|
56
|
-
:class="{
|
|
57
|
-
'is-active': editor.isActive(item.isActiveKey || item.stateKey, item.commandProps),
|
|
58
|
-
}"
|
|
54
|
+
:class="{ 'is-active': isItemActive(item) }"
|
|
59
55
|
:disabled="disabled || item.disabled"
|
|
60
56
|
@click="() => onMenuBarButtonClick(item)"
|
|
61
57
|
>
|
|
@@ -96,6 +92,7 @@ import Image from '@tiptap/extension-image'
|
|
|
96
92
|
import Superscript from '@tiptap/extension-superscript'
|
|
97
93
|
import CallToAction from './extensions/cta'
|
|
98
94
|
import StarterKit from '@tiptap/starter-kit'
|
|
95
|
+
import { LineCommands, LineShortcuts, isLineActive, runLineCommand } from './lines'
|
|
99
96
|
|
|
100
97
|
import { Options } from '../index'
|
|
101
98
|
|
|
@@ -123,7 +120,7 @@ export default {
|
|
|
123
120
|
},
|
|
124
121
|
watch: {
|
|
125
122
|
value(value) {
|
|
126
|
-
if (this.
|
|
123
|
+
if (this.getValue() === value) return
|
|
127
124
|
this.editor.commands.setContent(this.value, false)
|
|
128
125
|
},
|
|
129
126
|
disabled(value) {
|
|
@@ -273,8 +270,23 @@ export default {
|
|
|
273
270
|
.filter(Boolean)
|
|
274
271
|
.filter(cmd => !appearance || cmd.appearance[appearance])
|
|
275
272
|
},
|
|
273
|
+
getValue() {
|
|
274
|
+
const { doc } = this.editor.state
|
|
275
|
+
if (Tiptap.isNodeEmpty(doc, { ignoreWhitespace: true }) && !/\S/.test(doc.textContent))
|
|
276
|
+
return ''
|
|
277
|
+
return this.editor.getHTML()
|
|
278
|
+
},
|
|
279
|
+
isItemActive(item) {
|
|
280
|
+
const name = item.isActiveKey || item.stateKey
|
|
281
|
+
if (LineCommands.includes(item.commandKey))
|
|
282
|
+
return isLineActive(this.editor.state, name, item.commandProps)
|
|
283
|
+
|
|
284
|
+
return this.editor.isActive(name, item.commandProps)
|
|
285
|
+
},
|
|
276
286
|
onMenuBarButtonClick(item) {
|
|
277
287
|
if (item.customFunc) return item.customFunc.call(this, { editor: this.editor })
|
|
288
|
+
if (LineCommands.includes(item.commandKey))
|
|
289
|
+
return runLineCommand(this.editor, item.commandKey, item.commandProps)
|
|
278
290
|
|
|
279
291
|
return this.editor.chain().focus()[item.commandKey](item.commandProps).run()
|
|
280
292
|
},
|
|
@@ -312,12 +324,7 @@ export default {
|
|
|
312
324
|
this.linkUrl = null
|
|
313
325
|
},
|
|
314
326
|
bubbleShouldShow({ editor }) {
|
|
315
|
-
return
|
|
316
|
-
this.getCommands()
|
|
317
|
-
.filter(a => a.appearance.bubble)
|
|
318
|
-
.map(a => a.stateKey)
|
|
319
|
-
.some(k => editor.isActive(k)) || !this.editor.state.selection.empty
|
|
320
|
-
)
|
|
327
|
+
return !editor.state.selection.empty
|
|
321
328
|
},
|
|
322
329
|
isInternalUrl(value) {
|
|
323
330
|
if (!typeof value === 'string') return
|
|
@@ -342,15 +349,14 @@ export default {
|
|
|
342
349
|
editable: !this.disabled,
|
|
343
350
|
extensions: [
|
|
344
351
|
StarterKit,
|
|
352
|
+
LineShortcuts,
|
|
345
353
|
...this.getCommands()
|
|
346
354
|
.map(({ extension }) => extension)
|
|
347
355
|
.flat()
|
|
348
356
|
.filter(Boolean),
|
|
349
357
|
],
|
|
350
358
|
content: this.value,
|
|
351
|
-
onUpdate: () =>
|
|
352
|
-
this.$emit('input', this.editor.getHTML())
|
|
353
|
-
},
|
|
359
|
+
onUpdate: () => this.$emit('input', this.getValue()),
|
|
354
360
|
onSelectionUpdate: ({ editor }) => {
|
|
355
361
|
if (!this.bubbleShouldShow({ editor })) this.bubbleState = null
|
|
356
362
|
},
|
package/src/lines.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { CommandManager, Extension, isActive } from '@tiptap/core'
|
|
2
|
+
import { EditorState, Selection, TextSelection } from '@tiptap/pm/state'
|
|
3
|
+
import { canSplit } from '@tiptap/pm/transform'
|
|
4
|
+
|
|
5
|
+
export const LineCommands = ['toggleHeading', 'toggleBulletList', 'toggleOrderedList']
|
|
6
|
+
|
|
7
|
+
const ListTypes = { toggleBulletList: 'bulletList', toggleOrderedList: 'orderedList' }
|
|
8
|
+
|
|
9
|
+
function isBreak(node) {
|
|
10
|
+
return Boolean(node) && node.type.name === 'hardBreak'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function closestList($pos) {
|
|
14
|
+
for (let depth = $pos.depth; depth > 0; depth--) {
|
|
15
|
+
const node = $pos.node(depth)
|
|
16
|
+
if (Object.values(ListTypes).includes(node.type.name)) return node
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function trimSelection(doc, from, to) {
|
|
21
|
+
while (from < to) {
|
|
22
|
+
const $to = doc.resolve(to)
|
|
23
|
+
if (isBreak($to.nodeBefore)) {
|
|
24
|
+
to--
|
|
25
|
+
continue
|
|
26
|
+
}
|
|
27
|
+
if ($to.parentOffset > 0) break
|
|
28
|
+
const previous = Selection.findFrom(doc.resolve($to.before()), -1, true)
|
|
29
|
+
if (!previous || previous.from < from) break
|
|
30
|
+
to = previous.from
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
while (from < to) {
|
|
34
|
+
const $from = doc.resolve(from)
|
|
35
|
+
if (isBreak($from.nodeAfter)) {
|
|
36
|
+
from++
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
if ($from.parentOffset < $from.parent.content.size) break
|
|
40
|
+
const next = Selection.findFrom(doc.resolve($from.after()), 1, true)
|
|
41
|
+
if (!next || next.from > to) break
|
|
42
|
+
from = next.from
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { from, to }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function splitSelectedLines(tr, commandKey) {
|
|
49
|
+
const { selection, doc } = tr
|
|
50
|
+
if (!(selection instanceof TextSelection)) return false
|
|
51
|
+
|
|
52
|
+
const { from, to } = trimSelection(doc, selection.from, selection.to)
|
|
53
|
+
const $from = doc.resolve(from)
|
|
54
|
+
const $to = doc.resolve(to)
|
|
55
|
+
|
|
56
|
+
const listType = ListTypes[commandKey]
|
|
57
|
+
const list = listType && closestList($from)
|
|
58
|
+
if (list && list.type.name !== listType) return false
|
|
59
|
+
|
|
60
|
+
const breaks = new Set()
|
|
61
|
+
let before
|
|
62
|
+
$from.parent.forEach((node, offset) => {
|
|
63
|
+
const pos = $from.start() + offset
|
|
64
|
+
if (isBreak(node) && pos < from) before = pos
|
|
65
|
+
})
|
|
66
|
+
if (before !== undefined) breaks.add(before)
|
|
67
|
+
|
|
68
|
+
if (listType)
|
|
69
|
+
doc.nodesBetween(from, to, (node, pos) => {
|
|
70
|
+
if (isBreak(node) && pos >= from && pos < to) breaks.add(pos)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
let after
|
|
74
|
+
$to.parent.forEach((node, offset) => {
|
|
75
|
+
const pos = $to.start() + offset
|
|
76
|
+
if (after === undefined && isBreak(node) && pos >= to) after = pos
|
|
77
|
+
})
|
|
78
|
+
if (after !== undefined) breaks.add(after)
|
|
79
|
+
|
|
80
|
+
if (!breaks.size && from === selection.from && to === selection.to) return false
|
|
81
|
+
|
|
82
|
+
const mapFrom = tr.steps.length
|
|
83
|
+
Array.from(breaks)
|
|
84
|
+
.sort((a, b) => b - a)
|
|
85
|
+
.forEach(pos => {
|
|
86
|
+
const $after = tr.doc.resolve(pos + 1)
|
|
87
|
+
const inListItem = $after.depth > 1 && $after.node(-1).type.name === 'listItem'
|
|
88
|
+
const depth = inListItem && canSplit(tr.doc, pos + 1, 2) ? 2 : 1
|
|
89
|
+
if (canSplit(tr.doc, pos + 1, depth)) tr.split(pos + 1, depth).delete(pos, pos + 1)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const mapping = tr.mapping.slice(mapFrom)
|
|
93
|
+
tr.setSelection(
|
|
94
|
+
TextSelection.between(tr.doc.resolve(mapping.map(from)), tr.doc.resolve(mapping.map(to))),
|
|
95
|
+
)
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function isLineActive(state, name, attributes) {
|
|
100
|
+
const { selection, doc, schema } = state
|
|
101
|
+
if (!(selection instanceof TextSelection)) return isActive(state, name, attributes)
|
|
102
|
+
|
|
103
|
+
const { from, to } = trimSelection(doc, selection.from, selection.to)
|
|
104
|
+
return isActive({ doc, schema, selection: TextSelection.create(doc, from, to) }, name, attributes)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function runLineCommand(editor, commandKey, commandProps) {
|
|
108
|
+
const { state } = editor
|
|
109
|
+
const { tr } = state
|
|
110
|
+
|
|
111
|
+
if (!splitSelectedLines(tr, commandKey))
|
|
112
|
+
return editor.chain().focus()[commandKey](commandProps).run()
|
|
113
|
+
|
|
114
|
+
const split = EditorState.create({
|
|
115
|
+
schema: state.schema,
|
|
116
|
+
doc: tr.doc,
|
|
117
|
+
selection: tr.selection,
|
|
118
|
+
plugins: state.plugins,
|
|
119
|
+
})
|
|
120
|
+
const { tr: commandTr } = split
|
|
121
|
+
const ok = new CommandManager({ editor, state: split })
|
|
122
|
+
.createChain(commandTr)
|
|
123
|
+
[commandKey](commandProps)
|
|
124
|
+
.run()
|
|
125
|
+
if (!ok) return false
|
|
126
|
+
|
|
127
|
+
commandTr.steps.forEach(step => tr.step(step))
|
|
128
|
+
tr.setSelection(Selection.fromJSON(tr.doc, commandTr.selection.toJSON()))
|
|
129
|
+
if (commandTr.storedMarksSet) tr.setStoredMarks(commandTr.storedMarks)
|
|
130
|
+
|
|
131
|
+
editor.view.dispatch(tr)
|
|
132
|
+
editor.commands.focus()
|
|
133
|
+
return true
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const LineShortcuts = Extension.create({
|
|
137
|
+
name: 'lineShortcuts',
|
|
138
|
+
priority: 1001,
|
|
139
|
+
|
|
140
|
+
addKeyboardShortcuts() {
|
|
141
|
+
const heading = this.editor.extensionManager.extensions.find(e => e.name === 'heading')
|
|
142
|
+
const shortcuts = {
|
|
143
|
+
'Mod-Shift-8': () => runLineCommand(this.editor, 'toggleBulletList'),
|
|
144
|
+
'Mod-Shift-7': () => runLineCommand(this.editor, 'toggleOrderedList'),
|
|
145
|
+
'Mod-Alt-0': () => runLineCommand(this.editor, 'setParagraph'),
|
|
146
|
+
}
|
|
147
|
+
heading.options.levels.forEach(level => {
|
|
148
|
+
shortcuts[`Mod-Alt-${level}`] = () => runLineCommand(this.editor, 'toggleHeading', { level })
|
|
149
|
+
})
|
|
150
|
+
return shortcuts
|
|
151
|
+
},
|
|
152
|
+
})
|