prosemirror-view 1.42.0 → 1.42.1
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/CHANGELOG.md +6 -0
- package/dist/index.cjs +1 -0
- package/dist/index.js +3 -0
- package/package.json +1 -1
- package/src/#clipboard.ts# +275 -0
- package/src/domcoords.ts +4 -1
package/CHANGELOG.md
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -202,6 +202,7 @@ function clientRect(node) {
|
|
|
202
202
|
};
|
|
203
203
|
}
|
|
204
204
|
function scrollRectIntoView(view, rect, startDOM) {
|
|
205
|
+
if (!nonZero(rect) && rect.left == 0) return;
|
|
205
206
|
var scrollThreshold = view.someProp("scrollThreshold") || 0,
|
|
206
207
|
scrollMargin = view.someProp("scrollMargin") || 5;
|
|
207
208
|
var doc = view.dom.ownerDocument;
|
package/dist/index.js
CHANGED
|
@@ -207,6 +207,9 @@ function clientRect(node) {
|
|
|
207
207
|
top: rect.top, bottom: rect.top + node.clientHeight * scaleY };
|
|
208
208
|
}
|
|
209
209
|
function scrollRectIntoView(view, rect, startDOM) {
|
|
210
|
+
// Skip empty rects with all sides at 0, for example, when the element has no CSS box (display: none)
|
|
211
|
+
if (!nonZero(rect) && rect.left == 0)
|
|
212
|
+
return;
|
|
210
213
|
let scrollThreshold = view.someProp("scrollThreshold") || 0, scrollMargin = view.someProp("scrollMargin") || 5;
|
|
211
214
|
let doc = view.dom.ownerDocument;
|
|
212
215
|
for (let parent = startDOM || view.dom;;) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import {Slice, Fragment, DOMParser, DOMSerializer, ResolvedPos, NodeType, Node} from "prosemirror-model"
|
|
2
|
+
import * as browser from "./browser"
|
|
3
|
+
import {EditorView} from "./index"
|
|
4
|
+
|
|
5
|
+
export function serializeForClipboard(view: EditorView, slice: Slice) {
|
|
6
|
+
view.someProp("transformCopied", f => { slice = f(slice!, view) })
|
|
7
|
+
|
|
8
|
+
let context = [], {content, openStart, openEnd} = slice
|
|
9
|
+
while (openStart > 1 && openEnd > 1 && content.childCount == 1 && content.firstChild!.childCount == 1) {
|
|
10
|
+
openStart--
|
|
11
|
+
openEnd--
|
|
12
|
+
let node = content.firstChild!
|
|
13
|
+
context.push(node.type.name, node.attrs != node.type.defaultAttrs ? node.attrs : null)
|
|
14
|
+
content = node.content
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let serializer = view.someProp("clipboardSerializer") || DOMSerializer.fromSchema(view.state.schema)
|
|
18
|
+
let doc = detachedDoc(), wrap = doc.createElement("div")
|
|
19
|
+
wrap.appendChild(serializer.serializeFragment(content, {document: doc}))
|
|
20
|
+
|
|
21
|
+
let firstChild = wrap.firstChild, needsWrap, wrappers = 0
|
|
22
|
+
while (firstChild && firstChild.nodeType == 1 && (needsWrap = wrapMap[firstChild.nodeName.toLowerCase()])) {
|
|
23
|
+
for (let i = needsWrap.length - 1; i >= 0; i--) {
|
|
24
|
+
let wrapper = doc.createElement(needsWrap[i])
|
|
25
|
+
while (wrap.firstChild) wrapper.appendChild(wrap.firstChild)
|
|
26
|
+
wrap.appendChild(wrapper)
|
|
27
|
+
wrappers++
|
|
28
|
+
}
|
|
29
|
+
firstChild = wrap.firstChild
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (firstChild && firstChild.nodeType == 1)
|
|
33
|
+
(firstChild as HTMLElement).setAttribute(
|
|
34
|
+
"data-pm-slice", `${openStart} ${openEnd}${wrappers ? ` -${wrappers}` : ""} ${JSON.stringify(context)}`)
|
|
35
|
+
|
|
36
|
+
let text = view.someProp("clipboardTextSerializer", f => f(slice, view)) ||
|
|
37
|
+
slice.content.textBetween(0, slice.content.size, "\n\n")
|
|
38
|
+
|
|
39
|
+
return {dom: wrap, text, slice}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Read a slice of content from the clipboard (or drop data).
|
|
43
|
+
export function parseFromClipboard(view: EditorView, text: string, html: string | null, plainText: boolean, $context: ResolvedPos) {
|
|
44
|
+
let inCode = $context.parent.type.spec.code
|
|
45
|
+
let dom: HTMLElement | undefined, slice: Slice | undefined
|
|
46
|
+
if (!html && !text) return null
|
|
47
|
+
let asText = !!text && (plainText || inCode || !html)
|
|
48
|
+
if (asText) {
|
|
49
|
+
view.someProp("transformPastedText", f => { text = f(text, inCode || plainText, view) })
|
|
50
|
+
if (inCode) {
|
|
51
|
+
slice = new Slice(Fragment.from(view.state.schema.text(text.replace(/\r\n?/g, "\n"))), 0, 0)
|
|
52
|
+
view.someProp("transformPasted", f => { slice = f(slice!, view, true) })
|
|
53
|
+
return slice
|
|
54
|
+
}
|
|
55
|
+
let parsed = view.someProp("clipboardTextParser", f => f(text, $context, plainText, view))
|
|
56
|
+
if (parsed) {
|
|
57
|
+
slice = parsed
|
|
58
|
+
} else {
|
|
59
|
+
let marks = $context.marks()
|
|
60
|
+
let {schema} = view.state, serializer = DOMSerializer.fromSchema(schema)
|
|
61
|
+
dom = document.createElement("div")
|
|
62
|
+
text.split(/(?:\r\n?|\n)+/).forEach(block => {
|
|
63
|
+
let p = dom!.appendChild(document.createElement("p"))
|
|
64
|
+
if (block) p.appendChild(serializer.serializeNode(schema.text(block, marks)))
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
view.someProp("transformPastedHTML", f => { html = f(html!, view) })
|
|
69
|
+
dom = readHTML(html!)
|
|
70
|
+
if (browser.webkit) restoreReplacedSpaces(dom)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let contextNode = dom && dom.querySelector("[data-pm-slice]")
|
|
74
|
+
let sliceData = contextNode && /^(\d+) (\d+)(?: -(\d+))? (.*)/.exec(contextNode.getAttribute("data-pm-slice") || "")
|
|
75
|
+
if (sliceData && sliceData[3]) for (let i = +sliceData[3]; i > 0; i--) {
|
|
76
|
+
let child = dom!.firstChild
|
|
77
|
+
while (child && child.nodeType != 1) child = child.nextSibling
|
|
78
|
+
if (!child) break
|
|
79
|
+
dom = child as HTMLElement
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!slice) {
|
|
83
|
+
let parser = view.someProp("clipboardParser") || view.someProp("domParser") || DOMParser.fromSchema(view.state.schema)
|
|
84
|
+
slice = parser.parseSlice(dom!, {
|
|
85
|
+
preserveWhitespace: !!(asText || sliceData),
|
|
86
|
+
context: $context,
|
|
87
|
+
ruleFromNode(dom) {
|
|
88
|
+
if (dom.nodeName == "BR" && !dom.nextSibling &&
|
|
89
|
+
dom.parentNode && !inlineParents.test(dom.parentNode.nodeName)) return {ignore: true}
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
if (sliceData) {
|
|
95
|
+
slice = addContext(closeSlice(slice, +sliceData[1], +sliceData[2]), sliceData[4])
|
|
96
|
+
} else { // HTML wasn't created by ProseMirror. Make sure top-level siblings are coherent
|
|
97
|
+
slice = Slice.maxOpen(normalizeSiblings(slice.content, $context), true)
|
|
98
|
+
if (slice.openStart || slice.openEnd) {
|
|
99
|
+
let openStart = 0, openEnd = 0
|
|
100
|
+
for (let node = slice.content.firstChild; openStart < slice.openStart && !node!.type.spec.isolating;
|
|
101
|
+
openStart++, node = node!.firstChild) {}
|
|
102
|
+
for (let node = slice.content.lastChild; openEnd < slice.openEnd && !node!.type.spec.isolating;
|
|
103
|
+
openEnd++, node = node!.lastChild) {}
|
|
104
|
+
slice = closeSlice(slice, openStart, openEnd)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
view.someProp("transformPasted", f => { slice = f(slice!, view, asText) })
|
|
109
|
+
return slice
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const inlineParents = /^(a|abbr|acronym|b|cite|code|del|em|i|ins|kbd|label|output|q|ruby|s|samp|span|strong|sub|sup|time|u|tt|var)$/i
|
|
113
|
+
|
|
114
|
+
// Takes a slice parsed with parseSlice, which means there hasn't been
|
|
115
|
+
// any content-expression checking done on the top nodes, tries to
|
|
116
|
+
// find a parent node in the current context that might fit the nodes,
|
|
117
|
+
// and if successful, rebuilds the slice so that it fits into that parent.
|
|
118
|
+
//
|
|
119
|
+
// This addresses the problem that Transform.replace expects a
|
|
120
|
+
// coherent slice, and will fail to place a set of siblings that don't
|
|
121
|
+
// fit anywhere in the schema.
|
|
122
|
+
function normalizeSiblings(fragment: Fragment, $context: ResolvedPos) {
|
|
123
|
+
if (fragment.childCount < 2) return fragment
|
|
124
|
+
for (let d = $context.depth; d >= 0; d--) {
|
|
125
|
+
let parent = $context.node(d)
|
|
126
|
+
let match = parent.contentMatchAt($context.index(d))
|
|
127
|
+
let lastWrap: readonly NodeType[] | undefined, result: Node[] | null = []
|
|
128
|
+
fragment.forEach(node => {
|
|
129
|
+
if (!result) return
|
|
130
|
+
let wrap = match.findWrapping(node.type), inLast
|
|
131
|
+
if (!wrap) return result = null
|
|
132
|
+
if (inLast = result.length && lastWrap!.length && addToSibling(wrap, lastWrap!, node, result[result.length - 1], 0)) {
|
|
133
|
+
result[result.length - 1] = inLast
|
|
134
|
+
} else {
|
|
135
|
+
if (result.length) result[result.length - 1] = closeRight(result[result.length - 1], lastWrap!.length)
|
|
136
|
+
let wrapped = withWrappers(node, wrap)
|
|
137
|
+
result.push(wrapped)
|
|
138
|
+
match = match.matchType(wrapped.type)!
|
|
139
|
+
lastWrap = wrap
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
if (result) return Fragment.from(result)
|
|
143
|
+
}
|
|
144
|
+
return fragment
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function withWrappers(node: Node, wrap: readonly NodeType[], from = 0) {
|
|
148
|
+
for (let i = wrap.length - 1; i >= from; i--)
|
|
149
|
+
node = wrap[i].create(null, Fragment.from(node))
|
|
150
|
+
return node
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Used to group adjacent nodes wrapped in similar parents by
|
|
154
|
+
// normalizeSiblings into the same parent node
|
|
155
|
+
function addToSibling(wrap: readonly NodeType[], lastWrap: readonly NodeType[],
|
|
156
|
+
node: Node, sibling: Node, depth: number): Node | undefined {
|
|
157
|
+
if (depth < wrap.length && depth < lastWrap.length && wrap[depth] == lastWrap[depth]) {
|
|
158
|
+
let inner = addToSibling(wrap, lastWrap, node, sibling.lastChild!, depth + 1)
|
|
159
|
+
if (inner) return sibling.copy(sibling.content.replaceChild(sibling.childCount - 1, inner))
|
|
160
|
+
let match = sibling.contentMatchAt(sibling.childCount)
|
|
161
|
+
if (match.matchType(depth == wrap.length - 1 ? node.type : wrap[depth + 1]))
|
|
162
|
+
return sibling.copy(sibling.content.append(Fragment.from(withWrappers(node, wrap, depth + 1))))
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function closeRight(node: Node, depth: number) {
|
|
167
|
+
if (depth == 0) return node
|
|
168
|
+
let fragment = node.content.replaceChild(node.childCount - 1, closeRight(node.lastChild!, depth - 1))
|
|
169
|
+
let fill = node.contentMatchAt(node.childCount).fillBefore(Fragment.empty, true)!
|
|
170
|
+
return node.copy(fragment.append(fill))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function closeRange(fragment: Fragment, side: number, from: number, to: number, depth: number, openEnd: number) {
|
|
174
|
+
let node = side < 0 ? fragment.firstChild! : fragment.lastChild!, inner = node.content
|
|
175
|
+
if (fragment.childCount > 1) openEnd = 0
|
|
176
|
+
if (depth < to - 1) inner = closeRange(inner, side, from, to, depth + 1, openEnd)
|
|
177
|
+
if (depth >= from)
|
|
178
|
+
inner = side < 0 ? node.contentMatchAt(0)!.fillBefore(inner, openEnd <= depth)!.append(inner)
|
|
179
|
+
: inner.append(node.contentMatchAt(node.childCount)!.fillBefore(Fragment.empty, true)!)
|
|
180
|
+
return fragment.replaceChild(side < 0 ? 0 : fragment.childCount - 1, node.copy(inner))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function closeSlice(slice: Slice, openStart: number, openEnd: number) {
|
|
184
|
+
if (openStart < slice.openStart)
|
|
185
|
+
slice = new Slice(closeRange(slice.content, -1, openStart, slice.openStart, 0, slice.openEnd), openStart, slice.openEnd)
|
|
186
|
+
if (openEnd < slice.openEnd)
|
|
187
|
+
slice = new Slice(closeRange(slice.content, 1, openEnd, slice.openEnd, 0, 0), slice.openStart, openEnd)
|
|
188
|
+
return slice
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Trick from jQuery -- some elements must be wrapped in other
|
|
192
|
+
// elements for innerHTML to work. I.e. if you do `div.innerHTML =
|
|
193
|
+
// "<td>..</td>"` the table cells are ignored.
|
|
194
|
+
const wrapMap: {[node: string]: string[]} = {
|
|
195
|
+
thead: ["table"],
|
|
196
|
+
tbody: ["table"],
|
|
197
|
+
tfoot: ["table"],
|
|
198
|
+
caption: ["table"],
|
|
199
|
+
colgroup: ["table"],
|
|
200
|
+
col: ["table", "colgroup"],
|
|
201
|
+
tr: ["table", "tbody"],
|
|
202
|
+
td: ["table", "tbody", "tr"],
|
|
203
|
+
th: ["table", "tbody", "tr"]
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function detachedDoc() {
|
|
207
|
+
return document.implementation.createHTMLDocument("title")
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let _policy: any = null
|
|
211
|
+
|
|
212
|
+
function maybeWrapTrusted(html: string): string {
|
|
213
|
+
let trustedTypes = (window as any).trustedTypes
|
|
214
|
+
if (!trustedTypes) return html
|
|
215
|
+
// With the require-trusted-types-for CSP, Chrome will block
|
|
216
|
+
// innerHTML, even on a detached document. This wraps the string in
|
|
217
|
+
// a way that makes the browser allow us to use its parser again.
|
|
218
|
+
if (!_policy)
|
|
219
|
+
_policy = trustedTypes.defaultPolicy || trustedTypes.createPolicy("ProseMirrorClipboard", {createHTML: (s: string) => s})
|
|
220
|
+
return _policy.createHTML(html)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function readHTML(html: string) {
|
|
224
|
+
html = "\n\n" + html + "\n\n"
|
|
225
|
+
let metas = /^(\s*<meta [^>]*>)*/.exec(html)
|
|
226
|
+
if (metas) html = html.slice(metas[0].length)
|
|
227
|
+
let doc = detachedDoc(), elt = doc.createElement("div")
|
|
228
|
+
let firstTag = /<([a-z][^>\s]+)/i.exec(html), wrap
|
|
229
|
+
if (wrap = firstTag && wrapMap[firstTag[1].toLowerCase()])
|
|
230
|
+
html = wrap.map(n => "<" + n + ">").join("") + html + wrap.map(n => "</" + n + ">").reverse().join("")
|
|
231
|
+
elt.innerHTML = maybeWrapTrusted(html)
|
|
232
|
+
if (wrap) for (let i = 0; i < wrap.length; i++) elt = elt.querySelector(wrap[i]) || elt
|
|
233
|
+
// Inline styles defined in the pasted content, so that parse rules pick them up
|
|
234
|
+
for (let i = 0; i < doc.styleSheets.length; i++) {
|
|
235
|
+
let style = doc.styleSheets[i]
|
|
236
|
+
for (let j = 0; j < style.rules.length; j++) {
|
|
237
|
+
let rule = style.rules[j]
|
|
238
|
+
if (rule instanceof CSSStyleRule) {
|
|
239
|
+
let matches = elt.querySelectorAll(rule.selectorText)
|
|
240
|
+
for (let k = 0; k < matches.length; k++)
|
|
241
|
+
(matches[k] as HTMLElement).style.cssText += rule.style.cssText
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return elt
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Webkit browsers do some hard-to-predict replacement of regular
|
|
249
|
+
// spaces with non-breaking spaces when putting content on the
|
|
250
|
+
// clipboard. This tries to convert such non-breaking spaces (which
|
|
251
|
+
// will be wrapped in a plain span on Chrome, a span with class
|
|
252
|
+
// Apple-converted-space on Safari) back to regular spaces.
|
|
253
|
+
function restoreReplacedSpaces(dom: HTMLElement) {
|
|
254
|
+
let nodes = dom.querySelectorAll(browser.chrome ? "span:not([class]):not([style])" : "span.Apple-converted-space")
|
|
255
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
256
|
+
let node = nodes[i]
|
|
257
|
+
if (node.childNodes.length == 1 && node.textContent == "\u00a0" && node.parentNode)
|
|
258
|
+
node.parentNode.replaceChild(dom.ownerDocument.createTextNode(" "), node)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function addContext(slice: Slice, context: string) {
|
|
263
|
+
if (!slice.size) return slice
|
|
264
|
+
let schema = slice.content.firstChild!.type.schema, array
|
|
265
|
+
try { array = JSON.parse(context) }
|
|
266
|
+
catch(e) { return slice }
|
|
267
|
+
let {content, openStart, openEnd} = slice
|
|
268
|
+
for (let i = array.length - 2; i >= 0; i -= 2) {
|
|
269
|
+
let type = schema.nodes[array[i]]
|
|
270
|
+
if (!type || type.hasRequiredAttrs()) break
|
|
271
|
+
content = Fragment.from(type.create(array[i + 1], content))
|
|
272
|
+
openStart++; openEnd++
|
|
273
|
+
}
|
|
274
|
+
return new Slice(content, openStart, openEnd)
|
|
275
|
+
}
|
package/src/domcoords.ts
CHANGED
|
@@ -30,6 +30,9 @@ function clientRect(node: HTMLElement): Rect {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export function scrollRectIntoView(view: EditorView, rect: Rect, startDOM: Node) {
|
|
33
|
+
// Skip empty rects with all sides at 0, for example, when the element has no CSS box (display: none)
|
|
34
|
+
if (!nonZero(rect) && rect.left == 0) return
|
|
35
|
+
|
|
33
36
|
let scrollThreshold = view.someProp("scrollThreshold") || 0, scrollMargin = view.someProp("scrollMargin") || 5
|
|
34
37
|
let doc = view.dom.ownerDocument
|
|
35
38
|
for (let parent: Node | null = startDOM || view.dom;;) {
|
|
@@ -328,7 +331,7 @@ export function posAtCoords(view: EditorView, coords: {top: number, left: number
|
|
|
328
331
|
return {pos, inside: desc ? desc.posAtStart - desc.border : -1}
|
|
329
332
|
}
|
|
330
333
|
|
|
331
|
-
function nonZero(rect:
|
|
334
|
+
function nonZero(rect: Rect) {
|
|
332
335
|
return rect.top < rect.bottom || rect.left < rect.right
|
|
333
336
|
}
|
|
334
337
|
|