tiptapify 0.3.4 → 0.4.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.
Files changed (137) hide show
  1. package/dist/content.css +28 -0
  2. package/dist/tiptapify.css +1 -1
  3. package/dist/tiptapify.mjs +110820 -65669
  4. package/dist/tiptapify.umd.js +106 -89
  5. package/index.d.ts +34 -0
  6. package/package.json +3 -2
  7. package/src/components/ImageBubbleMenu.vue +5 -33
  8. package/src/components/MenuBubble.test.ts +120 -0
  9. package/src/components/MenuBubble.vue +124 -134
  10. package/src/components/MenuFloating.vue +48 -45
  11. package/src/components/Tiptapify.upload.test.ts +10 -0
  12. package/src/components/Tiptapify.vue +85 -45
  13. package/src/components/Toolbar/Index.vue +10 -0
  14. package/src/components/UI/BubbleRow.vue +41 -0
  15. package/src/components/UI/TiptapifyDialog.test.ts +37 -3
  16. package/src/components/UI/TiptapifyDialog.vue +15 -4
  17. package/src/components/editorExtensions.test.ts +9 -0
  18. package/src/components/editorExtensions.ts +21 -1
  19. package/src/composables/fakeUploadEndpoint.ts +65 -0
  20. package/src/composables/useTiptapifyConfig.ts +19 -0
  21. package/src/composables/useTiptapifyLinkPreview.test.ts +51 -0
  22. package/src/composables/useTiptapifyLinkPreview.ts +60 -0
  23. package/src/composables/useTiptapifyUpload.test.ts +96 -0
  24. package/src/composables/useTiptapifyUpload.ts +126 -0
  25. package/src/extensions/components/ai/Dialog.test.ts +335 -0
  26. package/src/extensions/components/ai/Dialog.vue +607 -310
  27. package/src/extensions/components/ai/backend.test.ts +1 -0
  28. package/src/extensions/components/ai/backend.ts +2 -1
  29. package/src/extensions/components/ai/context.test.ts +149 -0
  30. package/src/extensions/components/ai/context.ts +145 -0
  31. package/src/extensions/components/ai/diff.test.ts +65 -0
  32. package/src/extensions/components/ai/diff.ts +120 -0
  33. package/src/extensions/components/ai/errors.test.ts +45 -0
  34. package/src/extensions/components/ai/errors.ts +67 -0
  35. package/src/extensions/components/ai/result.test.ts +144 -0
  36. package/src/extensions/components/ai/result.ts +101 -0
  37. package/src/extensions/components/ai/thinking.test.ts +45 -0
  38. package/src/extensions/components/ai/thinking.ts +58 -0
  39. package/src/extensions/components/formatExtra/sub/Button.vue +1 -1
  40. package/src/extensions/components/formatExtra/sup/Button.vue +1 -1
  41. package/src/extensions/components/media/iframe/Button.vue +6 -7
  42. package/src/extensions/components/media/iframe/IframeDialog.test.ts +177 -0
  43. package/src/extensions/components/media/iframe/IframeDialog.vue +477 -105
  44. package/src/extensions/components/media/iframe/embed.test.ts +123 -0
  45. package/src/extensions/components/media/iframe/embed.ts +236 -0
  46. package/src/extensions/components/media/iframe/index.test.ts +123 -0
  47. package/src/extensions/components/media/iframe/index.ts +92 -10
  48. package/src/extensions/components/media/image/ImageDialog.vue +430 -132
  49. package/src/extensions/components/media/image/caption.test.ts +66 -1
  50. package/src/extensions/components/media/image/caption.ts +27 -1
  51. package/src/extensions/components/media/image/imageDialog.test.ts +200 -0
  52. package/src/extensions/components/media/image/imageDialog.ts +114 -0
  53. package/src/extensions/components/media/image/index.ts +2 -1
  54. package/src/extensions/components/media/link/Button.test.ts +96 -0
  55. package/src/extensions/components/media/link/Button.vue +6 -2
  56. package/src/extensions/components/media/link/LinkDialog.display.test.ts +363 -0
  57. package/src/extensions/components/media/link/LinkDialog.vue +687 -61
  58. package/src/extensions/components/media/link/index.ts +111 -6
  59. package/src/extensions/components/media/link/link.marks.test.ts +89 -0
  60. package/src/extensions/components/media/link/linkDialog.test.ts +296 -0
  61. package/src/extensions/components/media/link/linkDialog.ts +154 -0
  62. package/src/extensions/components/media/linkCard/PasteCardMenu.test.ts +133 -0
  63. package/src/extensions/components/media/linkCard/PasteCardMenu.vue +168 -0
  64. package/src/extensions/components/media/linkCard/index.test.ts +146 -0
  65. package/src/extensions/components/media/linkCard/index.ts +177 -0
  66. package/src/extensions/components/media/linkCard/linkCardDialog.test.ts +87 -0
  67. package/src/extensions/components/media/linkCard/linkCardDialog.ts +62 -0
  68. package/src/extensions/components/media/linkCard/linkPreview.test.ts +102 -0
  69. package/src/extensions/components/media/linkCard/linkPreview.ts +84 -0
  70. package/src/extensions/components/media/linkCard/makeLinkCard.test.ts +59 -0
  71. package/src/extensions/components/media/linkCard/makeLinkCard.ts +58 -0
  72. package/src/extensions/components/media/linkCard/pasteCard.test.ts +190 -0
  73. package/src/extensions/components/media/linkCard/pasteCard.ts +153 -0
  74. package/src/extensions/components/media/video/Button.vue +2 -1
  75. package/src/extensions/components/media/video/VideoDialog.test.ts +233 -0
  76. package/src/extensions/components/media/video/VideoDialog.vue +748 -59
  77. package/src/extensions/components/media/video/index.test.ts +154 -0
  78. package/src/extensions/components/media/video/index.ts +70 -5
  79. package/src/extensions/components/media/video/providers.test.ts +148 -0
  80. package/src/extensions/components/media/video/providers.ts +241 -0
  81. package/src/extensions/components/media/video/videoFile.test.ts +98 -0
  82. package/src/extensions/components/media/video/videoFile.ts +142 -0
  83. package/src/extensions/components/misc/fullscreen/Button.test.ts +55 -0
  84. package/src/extensions/components/misc/fullscreen/Button.vue +2 -2
  85. package/src/extensions/components/style/StyleColor.test.ts +255 -0
  86. package/src/extensions/components/style/StyleColor.vue +358 -208
  87. package/src/extensions/components/style/color/Button.vue +1 -3
  88. package/src/extensions/components/style/color/colors.test.ts +191 -0
  89. package/src/extensions/components/style/color/colors.ts +168 -0
  90. package/src/extensions/components/style/highlight/Button.vue +1 -3
  91. package/src/i18n/locales/ar.json +226 -23
  92. package/src/i18n/locales/bg.json +225 -22
  93. package/src/i18n/locales/ch.json +226 -23
  94. package/src/i18n/locales/cz.json +226 -23
  95. package/src/i18n/locales/de.json +226 -23
  96. package/src/i18n/locales/el.json +225 -22
  97. package/src/i18n/locales/en.json +225 -22
  98. package/src/i18n/locales/es.json +226 -23
  99. package/src/i18n/locales/et.json +225 -22
  100. package/src/i18n/locales/fi.json +226 -23
  101. package/src/i18n/locales/fr.json +226 -23
  102. package/src/i18n/locales/he.json +225 -22
  103. package/src/i18n/locales/hi.json +225 -22
  104. package/src/i18n/locales/hr.json +225 -22
  105. package/src/i18n/locales/hu.json +226 -23
  106. package/src/i18n/locales/id.json +225 -22
  107. package/src/i18n/locales/it.json +226 -23
  108. package/src/i18n/locales/ja.json +226 -23
  109. package/src/i18n/locales/ko.json +226 -23
  110. package/src/i18n/locales/la.json +226 -23
  111. package/src/i18n/locales/lt.json +226 -23
  112. package/src/i18n/locales/nl.json +226 -23
  113. package/src/i18n/locales/no.json +227 -24
  114. package/src/i18n/locales/pl.json +226 -23
  115. package/src/i18n/locales/pt-BR.json +225 -22
  116. package/src/i18n/locales/pt.json +226 -23
  117. package/src/i18n/locales/ro.json +225 -22
  118. package/src/i18n/locales/ru.json +227 -24
  119. package/src/i18n/locales/se.json +226 -23
  120. package/src/i18n/locales/sk.json +225 -22
  121. package/src/i18n/locales/sl.json +225 -22
  122. package/src/i18n/locales/sr.json +225 -22
  123. package/src/i18n/locales/th.json +226 -23
  124. package/src/i18n/locales/tr.json +226 -23
  125. package/src/i18n/locales/uk.json +227 -24
  126. package/src/i18n/locales/vi.json +226 -23
  127. package/src/index.ts +8 -0
  128. package/src/skins/registry.test.ts +1 -1
  129. package/src/skins/tokens.reachability.test.ts +1 -0
  130. package/src/skins/tokens.ts +3 -3
  131. package/src/types/editor.ts +40 -1
  132. package/src/utils/contentStyles.test.ts +116 -0
  133. package/src/utils/contentStyles.ts +175 -0
  134. package/src/utils/mediaSize.test.ts +22 -0
  135. package/src/utils/mediaSize.ts +27 -0
  136. package/src/utils/validateHref.test.ts +72 -2
  137. package/src/utils/validateHref.ts +57 -13
@@ -0,0 +1,191 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+ import { Editor } from '@tiptap/vue-3'
3
+ import { Document } from '@tiptap/extension-document'
4
+ import { Highlight } from '@tiptap/extension-highlight'
5
+ import { Paragraph } from '@tiptap/extension-paragraph'
6
+ import { Text } from '@tiptap/extension-text'
7
+ import { TextStyleKit } from '@tiptap/extension-text-style'
8
+ import { UndoRedo } from '@tiptap/extensions'
9
+ import {
10
+ addRecent,
11
+ loadRecent,
12
+ moveInGrid,
13
+ normalizeHex,
14
+ palettes,
15
+ RECENT_LIMIT,
16
+ sameColor,
17
+ saveRecent,
18
+ writeColor,
19
+ } from '@tiptapify/extensions/components/style/color/colors'
20
+
21
+ describe('palettes', () => {
22
+ it('start with the cell that removes the color, then rows of at most 8 valid colors', () => {
23
+ for (const rows of Object.values(palettes)) {
24
+ expect(rows[0][0].value).toBeNull()
25
+
26
+ const colors = rows.flat().slice(1).map(swatch => swatch.value)
27
+ expect(colors.every(color => normalizeHex(color) === color)).toBe(true)
28
+ expect(new Set(colors).size).toBe(colors.length)
29
+ expect(rows.every(row => row.length <= 8)).toBe(true)
30
+ }
31
+ })
32
+
33
+ it('name every swatch', () => {
34
+ const names = Object.values(palettes).flat(2).map(swatch => swatch.name)
35
+
36
+ expect(names.every(Boolean)).toBe(true)
37
+ expect(names).toContain('default')
38
+ expect(names).toContain('no_highlight')
39
+ expect(names).toContain('names.dark_red')
40
+ })
41
+ })
42
+
43
+ describe('normalizeHex', () => {
44
+ it.each([
45
+ ['#abc', '#aabbcc'],
46
+ ['#AABBCC', '#aabbcc'],
47
+ [' #0f0 ', '#00ff00'],
48
+ ['1D4ED8', '#1d4ed8'],
49
+ ])('reads %s as %s', (input, expected) => {
50
+ expect(normalizeHex(input)).toBe(expected)
51
+ })
52
+
53
+ it.each(['', '#', '#ab', '#abcd', '#abcdeg', 'red', 'rgb(0, 0, 0)', null, undefined])('rejects %s', input => {
54
+ expect(normalizeHex(input)).toBeNull()
55
+ })
56
+ })
57
+
58
+ describe('sameColor', () => {
59
+ it('compares hex colors in any notation and other colors as text', () => {
60
+ expect(sameColor('#FFF', '#ffffff')).toBe(true)
61
+ expect(sameColor('#fff', '#fffffe')).toBe(false)
62
+ expect(sameColor('rgb(0, 0, 0)', 'RGB(0, 0, 0)')).toBe(true)
63
+ expect(sameColor('rgb(0, 0, 0)', '#000000')).toBe(false)
64
+ })
65
+ })
66
+
67
+ describe('recent colors', () => {
68
+ afterEach(() => {
69
+ localStorage.clear()
70
+ vi.restoreAllMocks()
71
+ })
72
+
73
+ it('puts the newest first without duplicates and keeps the limit', () => {
74
+ let list: string[] = []
75
+ for (let index = 0; index < RECENT_LIMIT + 2; index++) {
76
+ list = addRecent(list, `#00000${index}`)
77
+ }
78
+
79
+ expect(list).toHaveLength(RECENT_LIMIT)
80
+ expect(list[0]).toBe('#000009')
81
+ expect(addRecent(list, '#000005')[0]).toBe('#000005')
82
+ expect(addRecent(list, '#000005')).toHaveLength(RECENT_LIMIT)
83
+ expect(addRecent(['#aabbcc'], '#ABC')).toEqual(['#aabbcc'])
84
+ expect(addRecent(['#aabbcc'], 'nope')).toEqual(['#aabbcc'])
85
+ })
86
+
87
+ it('are remembered per kind', () => {
88
+ saveRecent('text', ['#b91c1c', '#1d4ed8'])
89
+
90
+ expect(loadRecent('text')).toEqual(['#b91c1c', '#1d4ed8'])
91
+ expect(loadRecent('highlight')).toEqual([])
92
+ })
93
+
94
+ it('skip junk in storage', () => {
95
+ localStorage.setItem('tiptapify:recent-colors:text', JSON.stringify(['#ABC', 'red', 42, '#aabbcc']))
96
+ expect(loadRecent('text')).toEqual(['#aabbcc'])
97
+
98
+ localStorage.setItem('tiptapify:recent-colors:text', '{broken')
99
+ expect(loadRecent('text')).toEqual([])
100
+
101
+ localStorage.setItem('tiptapify:recent-colors:text', '{"a":1}')
102
+ expect(loadRecent('text')).toEqual([])
103
+ })
104
+
105
+ it('work without storage', () => {
106
+ vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
107
+ throw new Error('blocked')
108
+ })
109
+ vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
110
+ throw new Error('blocked')
111
+ })
112
+
113
+ expect(() => saveRecent('text', ['#000000'])).not.toThrow()
114
+ expect(loadRecent('text')).toEqual([])
115
+ })
116
+ })
117
+
118
+ describe('moveInGrid', () => {
119
+ const rows = [3, 8, 8]
120
+
121
+ it('runs left/right through the rows like text', () => {
122
+ expect(moveInGrid(rows, { row: 0, col: 2 }, 'ArrowRight')).toEqual({ row: 1, col: 0 })
123
+ expect(moveInGrid(rows, { row: 1, col: 0 }, 'ArrowLeft')).toEqual({ row: 0, col: 2 })
124
+ expect(moveInGrid(rows, { row: 0, col: 0 }, 'ArrowLeft')).toEqual({ row: 0, col: 0 })
125
+ expect(moveInGrid(rows, { row: 2, col: 7 }, 'ArrowRight')).toEqual({ row: 2, col: 7 })
126
+ })
127
+
128
+ it('keeps the column up/down where the row is long enough', () => {
129
+ expect(moveInGrid(rows, { row: 1, col: 6 }, 'ArrowDown')).toEqual({ row: 2, col: 6 })
130
+ expect(moveInGrid(rows, { row: 1, col: 6 }, 'ArrowUp')).toEqual({ row: 0, col: 2 })
131
+ expect(moveInGrid(rows, { row: 0, col: 1 }, 'ArrowUp')).toEqual({ row: 0, col: 1 })
132
+ expect(moveInGrid(rows, { row: 2, col: 1 }, 'ArrowDown')).toEqual({ row: 2, col: 1 })
133
+ })
134
+
135
+ it('jumps to the row ends with Home/End and ignores other keys', () => {
136
+ expect(moveInGrid(rows, { row: 1, col: 4 }, 'Home')).toEqual({ row: 1, col: 0 })
137
+ expect(moveInGrid(rows, { row: 1, col: 4 }, 'End')).toEqual({ row: 1, col: 7 })
138
+ expect(moveInGrid(rows, { row: 1, col: 4 }, 'a')).toBeNull()
139
+ expect(moveInGrid(rows, { row: 5, col: 0 }, 'ArrowRight')).toBeNull()
140
+ })
141
+ })
142
+
143
+ describe('writeColor', () => {
144
+ let editor: Editor
145
+
146
+ function makeEditor(content: string): Editor {
147
+ editor = new Editor({
148
+ element: document.createElement('div'),
149
+ content,
150
+ extensions: [Document, Paragraph, Text, TextStyleKit, Highlight.configure({ multicolor: true }), UndoRedo],
151
+ })
152
+ editor.commands.selectAll()
153
+
154
+ return editor
155
+ }
156
+
157
+ afterEach(() => editor?.destroy())
158
+
159
+ it('sets and removes the text color', () => {
160
+ makeEditor('<p>Hello</p>')
161
+
162
+ writeColor(editor, 'text', '#b91c1c')
163
+ expect(editor.getAttributes('textStyle').color).toBe('#b91c1c')
164
+
165
+ writeColor(editor, 'text', null)
166
+ expect(editor.getAttributes('textStyle').color).toBeFalsy()
167
+ })
168
+
169
+ it('sets and removes the highlight', () => {
170
+ makeEditor('<p>Hello</p>')
171
+
172
+ writeColor(editor, 'highlight', '#fde68a')
173
+ expect(editor.getAttributes('highlight').color).toBe('#fde68a')
174
+
175
+ writeColor(editor, 'highlight', null)
176
+ expect(editor.isActive('highlight')).toBe(false)
177
+ })
178
+
179
+ it('keeps previews out of undo', () => {
180
+ makeEditor('<p><span style="color: #1d4ed8">Hello</span></p>')
181
+
182
+ writeColor(editor, 'text', '#b91c1c', { history: false })
183
+ expect(editor.getAttributes('textStyle').color).toBe('#b91c1c')
184
+ expect(editor.can().undo()).toBe(false)
185
+
186
+ writeColor(editor, 'text', '#1d4ed8', { history: false })
187
+ writeColor(editor, 'text', '#15803d')
188
+ editor.commands.undo()
189
+ expect(editor.getAttributes('textStyle').color).toBe('#1d4ed8')
190
+ })
191
+ })
@@ -0,0 +1,168 @@
1
+ import { Editor } from '@tiptap/vue-3'
2
+
3
+ export type ColorKind = 'text' | 'highlight'
4
+
5
+ // `value: null` is the "Default" / "No highlight" cell that removes the color; `name` is a key under `style.color`
6
+ export type Swatch = { value: string | null, name: string }
7
+
8
+ export type GridPosition = { row: number, col: number }
9
+
10
+ export const RECENT_LIMIT = 8
11
+
12
+ const hues = ['red', 'orange', 'amber', 'green', 'teal', 'blue', 'purple', 'pink']
13
+
14
+ function hueRow(prefix: string, values: string[]): Swatch[] {
15
+ return values.map((value, index) => ({ value, name: `names.${prefix}${hues[index]}` }))
16
+ }
17
+
18
+ // Rows of 8: text colors stay readable on a light page, highlights are pastel so black text stays readable on them
19
+ export const palettes: Record<ColorKind, Swatch[][]> = {
20
+ text: [
21
+ [
22
+ { value: null, name: 'default' },
23
+ { value: '#000000', name: 'names.black' },
24
+ { value: '#404040', name: 'names.dark_grey' },
25
+ { value: '#595959', name: 'names.grey' },
26
+ { value: '#808080', name: 'names.medium_grey' },
27
+ { value: '#a6a6a6', name: 'names.light_grey' },
28
+ { value: '#d9d9d9', name: 'names.pale_grey' },
29
+ { value: '#ffffff', name: 'names.white' },
30
+ ],
31
+ hueRow('dark_', ['#b91c1c', '#c2410c', '#a16207', '#15803d', '#0f766e', '#1d4ed8', '#7e22ce', '#be185d']),
32
+ hueRow('', ['#ef4444', '#f97316', '#eab308', '#22c55e', '#14b8a6', '#3b82f6', '#a855f7', '#ec4899']),
33
+ ],
34
+ highlight: [
35
+ [
36
+ { value: null, name: 'no_highlight' },
37
+ { value: '#ededed', name: 'names.pale_grey' },
38
+ { value: '#d4d4d4', name: 'names.light_grey' },
39
+ ],
40
+ hueRow('light_', ['#fecaca', '#fed7aa', '#fde68a', '#bbf7d0', '#99f6e4', '#bfdbfe', '#e9d5ff', '#fbcfe8']),
41
+ hueRow('', ['#fca5a5', '#fdba74', '#fcd34d', '#86efac', '#5eead4', '#93c5fd', '#d8b4fe', '#f9a8d4']),
42
+ ],
43
+ }
44
+
45
+ /**
46
+ * `#rgb` / `#rrggbb` (the `#` may be omitted, as copied from design tools) to lowercase `#rrggbb`, else null.
47
+ */
48
+ export function normalizeHex(input: string | null | undefined): string | null {
49
+ const match = /^#?([\da-f]{3}|[\da-f]{6})$/i.exec(String(input ?? '').trim())
50
+ if (!match) {
51
+ return null
52
+ }
53
+
54
+ const digits = match[1].length === 3 ? match[1].replace(/./g, digit => digit + digit) : match[1]
55
+
56
+ return `#${digits.toLowerCase()}`
57
+ }
58
+
59
+ // Colors pasted from other sites may be `rgb()` or names: those match only themselves
60
+ export function sameColor(a: string | null | undefined, b: string | null | undefined): boolean {
61
+ const left = normalizeHex(a) ?? String(a ?? '').trim().toLowerCase()
62
+ const right = normalizeHex(b) ?? String(b ?? '').trim().toLowerCase()
63
+
64
+ return left === right
65
+ }
66
+
67
+ export function addRecent(list: string[], color: string, limit = RECENT_LIMIT): string[] {
68
+ const hex = normalizeHex(color)
69
+ if (!hex) {
70
+ return list
71
+ }
72
+
73
+ return [hex, ...list.filter(item => item !== hex)].slice(0, limit)
74
+ }
75
+
76
+ function storage(): Storage | null {
77
+ try {
78
+ return window.localStorage
79
+ } catch {
80
+ return null
81
+ }
82
+ }
83
+
84
+ function recentKey(kind: ColorKind): string {
85
+ return `tiptapify:recent-colors:${kind}`
86
+ }
87
+
88
+ // Storage may be blocked (private mode, sandboxed iframe) or hold junk: the picker then starts without recent colors
89
+ export function loadRecent(kind: ColorKind): string[] {
90
+ try {
91
+ const stored = JSON.parse(storage()?.getItem(recentKey(kind)) ?? '[]')
92
+ if (!Array.isArray(stored)) {
93
+ return []
94
+ }
95
+
96
+ const colors = stored.map(item => normalizeHex(String(item))).filter((item): item is string => !!item)
97
+
98
+ return [...new Set(colors)].slice(0, RECENT_LIMIT)
99
+ } catch {
100
+ return []
101
+ }
102
+ }
103
+
104
+ export function saveRecent(kind: ColorKind, list: string[]): void {
105
+ try {
106
+ storage()?.setItem(recentKey(kind), JSON.stringify(list.slice(0, RECENT_LIMIT)))
107
+ } catch {
108
+ // remembered for this session only
109
+ }
110
+ }
111
+
112
+ /**
113
+ * The next swatch for an arrow / Home / End key in rows of different lengths, or null for other keys.
114
+ * Left/right run through the rows like text, up/down keep the column where the row is long enough.
115
+ */
116
+ export function moveInGrid(rowLengths: number[], position: GridPosition, key: string): GridPosition | null {
117
+ const flat = rowLengths.flatMap((length, row) => Array.from({ length }, (_, col) => ({ row, col })))
118
+ const index = flat.findIndex(cell => cell.row === position.row && cell.col === position.col)
119
+ if (index === -1 || !flat.length) {
120
+ return null
121
+ }
122
+
123
+ const rowAt = (row: number): GridPosition => ({ row, col: Math.min(position.col, rowLengths[row] - 1) })
124
+
125
+ switch (key) {
126
+ case 'ArrowRight':
127
+ return flat[Math.min(index + 1, flat.length - 1)]
128
+ case 'ArrowLeft':
129
+ return flat[Math.max(index - 1, 0)]
130
+ case 'ArrowDown':
131
+ return position.row < rowLengths.length - 1 ? rowAt(position.row + 1) : position
132
+ case 'ArrowUp':
133
+ return position.row > 0 ? rowAt(position.row - 1) : position
134
+ case 'Home':
135
+ return { row: position.row, col: 0 }
136
+ case 'End':
137
+ return { row: position.row, col: rowLengths[position.row] - 1 }
138
+ default:
139
+ return null
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Sets (or with null removes) the text or highlight color of the selection.
145
+ * A preview (`history: false`) is left out of undo, so only applied colors become undo steps.
146
+ */
147
+ export function writeColor(editor: Editor, kind: ColorKind, value: string | null, { history = true, focus = false } = {}): void {
148
+ let chain = editor.chain()
149
+ if (focus) {
150
+ chain = chain.focus()
151
+ }
152
+
153
+ if (kind === 'text') {
154
+ chain = value ? chain.setColor(value) : chain.unsetColor()
155
+ } else {
156
+ chain = value ? chain.setHighlight({ color: value }) : chain.unsetHighlight()
157
+ }
158
+
159
+ if (!history) {
160
+ chain = chain.command(({ tr }) => {
161
+ tr.setMeta('addToHistory', false)
162
+
163
+ return true
164
+ })
165
+ }
166
+
167
+ chain.run()
168
+ }
@@ -47,9 +47,7 @@ const selectedColor = computed(() => editor.value.getAttributes('highlight').col
47
47
  />
48
48
  </VBtn>
49
49
 
50
- <VMenu :activator="`#tiptapify-highlight-button-${editor.instanceId}`">
51
- <StyleColor :font-color="false" :background-color="true" :color="selectedColor" />
52
- </VMenu>
50
+ <StyleColor kind="highlight" :activator="`#tiptapify-highlight-button-${editor.instanceId}`" :color="selectedColor" />
53
51
  </template>
54
52
 
55
53
  <style lang="scss" scoped>