@kvass/rich-text 1.5.2 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kvass/rich-text",
3
- "version": "1.5.2",
3
+ "version": "1.5.3",
4
4
  "description": "rich text component for kvass",
5
5
  "main": "index.js",
6
6
  "scripts": {
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.editor.getHTML() === value) return
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,18 +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
- const html = this.editor.getHTML()
353
- if (/^<p>\s*<\/p>$/.test(html)) return this.$emit('input', '')
354
-
355
- return this.$emit('input', html)
356
- },
359
+ onUpdate: () => this.$emit('input', this.getValue()),
357
360
  onSelectionUpdate: ({ editor }) => {
358
361
  if (!this.bubbleShouldShow({ editor })) this.bubbleState = null
359
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
+ })