-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Expand file tree
/
Copy pathindex.ts
More file actions
187 lines (159 loc) · 5.21 KB
/
index.ts
File metadata and controls
187 lines (159 loc) · 5.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
import { Mark, mergeAttributes } from '@tiptap/core'
import type { TextStyleAttributes } from '../index.js'
export interface TextStyleOptions {
/**
* HTML attributes to add to the span element.
* @default {}
* @example { class: 'foo' }
*/
HTMLAttributes: Record<string, any>
/**
* When enabled, merges the styles of nested spans into the child span during HTML parsing.
* This prioritizes the style of the child span.
* Used when parsing content created in other editors.
* (Fix for ProseMirror's default behavior.)
* @default true
*/
mergeNestedSpanStyles: boolean
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
textStyle: {
/**
* Remove spans without inline style attributes.
* @example editor.commands.removeEmptyTextStyle()
*/
removeEmptyTextStyle: () => ReturnType
/**
* Toggle a text style
* @param attributes The text style attributes
* @example editor.commands.toggleTextStyle({ fontWeight: 'bold' })
*/
toggleTextStyle: (attributes?: TextStyleAttributes) => ReturnType
}
}
}
const MAX_FIND_CHILD_SPAN_DEPTH = 20
/**
* Returns all next child spans, either direct children or nested deeper
* but won't traverse deeper into child spans found, will only go MAX_FIND_CHILD_SPAN_DEPTH levels deep (default: 20)
*/
const findChildSpans = (element: HTMLElement, depth = 0): HTMLElement[] => {
const childSpans: HTMLElement[] = []
if (!element.children.length || depth > MAX_FIND_CHILD_SPAN_DEPTH) {
return childSpans
}
Array.from(element.children).forEach(child => {
if (child.tagName === 'SPAN') {
childSpans.push(child as HTMLElement)
} else if (child.children.length) {
childSpans.push(...findChildSpans(child as HTMLElement, depth + 1))
}
})
return childSpans
}
const mergeNestedSpanStyles = (element: HTMLElement) => {
if (!element.children.length) {
return
}
const childSpans = findChildSpans(element)
if (!childSpans) {
return
}
childSpans.forEach(childSpan => {
const childStyle = childSpan.getAttribute('style')
const closestParentSpanStyleOfChild = childSpan.parentElement?.closest('span')?.getAttribute('style')
childSpan.setAttribute('style', `${closestParentSpanStyleOfChild};${childStyle}`)
})
}
/**
* This extension allows you to create text styles. It is required by default
* for the `text-color` and `font-family` extensions.
* @see https://www.tiptap.dev/api/marks/text-style
*/
export const TextStyle = Mark.create<TextStyleOptions>({
name: 'textStyle',
priority: 101,
addOptions() {
return {
HTMLAttributes: {},
mergeNestedSpanStyles: true,
}
},
parseHTML() {
return [
{
tag: 'span',
consuming: false,
getAttrs: element => {
const hasStyles = (element as HTMLElement).hasAttribute('style')
if (!hasStyles) {
return false
}
if (this.options.mergeNestedSpanStyles) {
mergeNestedSpanStyles(element)
}
return {}
},
},
]
},
renderHTML({ HTMLAttributes }) {
return ['span', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
},
addMarkView() {
return ({ HTMLAttributes }) => {
const span = document.createElement('span')
// Use setAttribute instead of style.cssText to preserve original
// color format (e.g. hex #FF0000) and prevent browser normalization
// to rgb() which causes mark attribute mismatches during IME composition.
Object.entries(HTMLAttributes).forEach(([attr, value]) => {
if (value != null && value !== false) {
span.setAttribute(attr, String(value))
}
})
return {
dom: span,
contentDOM: span,
}
}
},
addCommands() {
return {
toggleTextStyle:
attributes =>
({ commands }) => {
return commands.toggleMark(this.name, attributes)
},
removeEmptyTextStyle:
() =>
({ tr }) => {
const { selection } = tr
// Gather all of the nodes within the selection range.
// We would need to go through each node individually
// to check if it has any inline style attributes.
// Otherwise, calling commands.unsetMark(this.name)
// removes everything from all the nodes
// within the selection range.
tr.doc.nodesBetween(selection.from, selection.to, (node, pos) => {
// Check if it's a paragraph element, if so, skip this node as we apply
// the text style to inline text nodes only (span).
if (node.isTextblock) {
return true
}
// Check if the node has no inline style attributes.
// Filter out non-`textStyle` marks.
if (
!node.marks
.filter(mark => mark.type === this.type)
.some(mark => Object.values(mark.attrs).some(value => !!value))
) {
// Proceed with the removal of the `textStyle` mark for this node only
tr.removeMark(pos, pos + node.nodeSize, this.type)
}
})
return true
},
}
},
})