@kvass/rich-text 1.5.2 → 1.6.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/package.json +1 -1
- package/src/Component.vue +24 -19
- package/src/lines.js +152 -0
package/package.json
CHANGED
package/src/Component.vue
CHANGED
|
@@ -32,10 +32,9 @@
|
|
|
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"
|
|
37
|
+
:title="labels[item.stateKey]"
|
|
39
38
|
@click="() => onMenuBarButtonClick(item)"
|
|
40
39
|
type="button"
|
|
41
40
|
>
|
|
@@ -53,10 +52,9 @@
|
|
|
53
52
|
v-for="(item, index) in getCommands('menu')"
|
|
54
53
|
:key="'menubar' + index"
|
|
55
54
|
type="button"
|
|
56
|
-
:class="{
|
|
57
|
-
'is-active': editor.isActive(item.isActiveKey || item.stateKey, item.commandProps),
|
|
58
|
-
}"
|
|
55
|
+
:class="{ 'is-active': isItemActive(item) }"
|
|
59
56
|
:disabled="disabled || item.disabled"
|
|
57
|
+
:title="labels[item.stateKey]"
|
|
60
58
|
@click="() => onMenuBarButtonClick(item)"
|
|
61
59
|
>
|
|
62
60
|
<FontAwesomeIcon :icon="item.icon" size="sm" />
|
|
@@ -96,6 +94,7 @@ import Image from '@tiptap/extension-image'
|
|
|
96
94
|
import Superscript from '@tiptap/extension-superscript'
|
|
97
95
|
import CallToAction from './extensions/cta'
|
|
98
96
|
import StarterKit from '@tiptap/starter-kit'
|
|
97
|
+
import { LineCommands, LineShortcuts, isLineActive, runLineCommand } from './lines'
|
|
99
98
|
|
|
100
99
|
import { Options } from '../index'
|
|
101
100
|
|
|
@@ -123,7 +122,7 @@ export default {
|
|
|
123
122
|
},
|
|
124
123
|
watch: {
|
|
125
124
|
value(value) {
|
|
126
|
-
if (this.
|
|
125
|
+
if (this.getValue() === value) return
|
|
127
126
|
this.editor.commands.setContent(this.value, false)
|
|
128
127
|
},
|
|
129
128
|
disabled(value) {
|
|
@@ -273,8 +272,23 @@ export default {
|
|
|
273
272
|
.filter(Boolean)
|
|
274
273
|
.filter(cmd => !appearance || cmd.appearance[appearance])
|
|
275
274
|
},
|
|
275
|
+
getValue() {
|
|
276
|
+
const { doc } = this.editor.state
|
|
277
|
+
if (Tiptap.isNodeEmpty(doc, { ignoreWhitespace: true }) && !/\S/.test(doc.textContent))
|
|
278
|
+
return ''
|
|
279
|
+
return this.editor.getHTML()
|
|
280
|
+
},
|
|
281
|
+
isItemActive(item) {
|
|
282
|
+
const name = item.isActiveKey || item.stateKey
|
|
283
|
+
if (LineCommands.includes(item.commandKey))
|
|
284
|
+
return isLineActive(this.editor.state, name, item.commandProps)
|
|
285
|
+
|
|
286
|
+
return this.editor.isActive(name, item.commandProps)
|
|
287
|
+
},
|
|
276
288
|
onMenuBarButtonClick(item) {
|
|
277
289
|
if (item.customFunc) return item.customFunc.call(this, { editor: this.editor })
|
|
290
|
+
if (LineCommands.includes(item.commandKey))
|
|
291
|
+
return runLineCommand(this.editor, item.commandKey, item.commandProps)
|
|
278
292
|
|
|
279
293
|
return this.editor.chain().focus()[item.commandKey](item.commandProps).run()
|
|
280
294
|
},
|
|
@@ -312,12 +326,7 @@ export default {
|
|
|
312
326
|
this.linkUrl = null
|
|
313
327
|
},
|
|
314
328
|
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
|
-
)
|
|
329
|
+
return !editor.state.selection.empty
|
|
321
330
|
},
|
|
322
331
|
isInternalUrl(value) {
|
|
323
332
|
if (!typeof value === 'string') return
|
|
@@ -342,18 +351,14 @@ export default {
|
|
|
342
351
|
editable: !this.disabled,
|
|
343
352
|
extensions: [
|
|
344
353
|
StarterKit,
|
|
354
|
+
LineShortcuts,
|
|
345
355
|
...this.getCommands()
|
|
346
356
|
.map(({ extension }) => extension)
|
|
347
357
|
.flat()
|
|
348
358
|
.filter(Boolean),
|
|
349
359
|
],
|
|
350
360
|
content: this.value,
|
|
351
|
-
onUpdate: () =>
|
|
352
|
-
const html = this.editor.getHTML()
|
|
353
|
-
if (/^<p>\s*<\/p>$/.test(html)) return this.$emit('input', '')
|
|
354
|
-
|
|
355
|
-
return this.$emit('input', html)
|
|
356
|
-
},
|
|
361
|
+
onUpdate: () => this.$emit('input', this.getValue()),
|
|
357
362
|
onSelectionUpdate: ({ editor }) => {
|
|
358
363
|
if (!this.bubbleShouldShow({ editor })) this.bubbleState = null
|
|
359
364
|
},
|
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
|
+
})
|