prosemirror-view 1.42.1 → 1.42.2
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 +8 -0
- package/dist/index.cjs +35 -28
- package/dist/index.js +18 -12
- package/package.json +1 -1
- package/src/domchange.ts +5 -5
- package/src/input.ts +2 -2
- package/src/viewdesc.ts +10 -3
- package/src/#clipboard.ts# +0 -275
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 1.42.2 (2026-07-24)
|
|
2
|
+
|
|
3
|
+
### Bug fixes
|
|
4
|
+
|
|
5
|
+
Enable a workaround for backspace not taking effect on Android for Firefox as well as Chrome (for which it already kicked in).
|
|
6
|
+
|
|
7
|
+
Work around the situation where Chrome/Webkit replace a node's content DOM with a completely different element.
|
|
8
|
+
|
|
1
9
|
## 1.42.1 (2026-07-11)
|
|
2
10
|
|
|
3
11
|
### Bug fixes
|
package/dist/index.cjs
CHANGED
|
@@ -687,7 +687,7 @@ var ViewDesc = function () {
|
|
|
687
687
|
}
|
|
688
688
|
}, {
|
|
689
689
|
key: "parseRule",
|
|
690
|
-
value: function parseRule() {
|
|
690
|
+
value: function parseRule(addedNodes) {
|
|
691
691
|
return null;
|
|
692
692
|
}
|
|
693
693
|
}, {
|
|
@@ -1285,7 +1285,7 @@ var NodeViewDesc = function (_ViewDesc4) {
|
|
|
1285
1285
|
}
|
|
1286
1286
|
_createClass(NodeViewDesc, [{
|
|
1287
1287
|
key: "parseRule",
|
|
1288
|
-
value: function parseRule() {
|
|
1288
|
+
value: function parseRule(addedNodes) {
|
|
1289
1289
|
var _this5 = this;
|
|
1290
1290
|
if (this.node.type.spec.reparseInView) return null;
|
|
1291
1291
|
var rule = {
|
|
@@ -1307,9 +1307,14 @@ var NodeViewDesc = function (_ViewDesc4) {
|
|
|
1307
1307
|
break;
|
|
1308
1308
|
}
|
|
1309
1309
|
}
|
|
1310
|
-
if (!rule.contentElement)
|
|
1311
|
-
|
|
1312
|
-
|
|
1310
|
+
if (!rule.contentElement) {
|
|
1311
|
+
var found = addedNodes && addedNodes.find(function (n) {
|
|
1312
|
+
return n.nodeType == 1 && addedNodes.indexOf(n.parentNode) < 0 && _this5.dom.contains(n);
|
|
1313
|
+
});
|
|
1314
|
+
if (found) rule.contentElement = found;else rule.getContent = function () {
|
|
1315
|
+
return prosemirrorModel.Fragment.empty;
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1313
1318
|
}
|
|
1314
1319
|
return rule;
|
|
1315
1320
|
}
|
|
@@ -3669,7 +3674,7 @@ handlers.blur = function (view, _event) {
|
|
|
3669
3674
|
};
|
|
3670
3675
|
handlers.beforeinput = function (view, _event) {
|
|
3671
3676
|
var event = _event;
|
|
3672
|
-
if (
|
|
3677
|
+
if (android && event.inputType == "deleteContentBackward") {
|
|
3673
3678
|
view.domObserver.flushSoon();
|
|
3674
3679
|
var domChangeCount = view.input.domChangeCount;
|
|
3675
3680
|
setTimeout(function () {
|
|
@@ -4706,7 +4711,7 @@ function fixUpBadSafariComposition(view, addedNodes) {
|
|
|
4706
4711
|
_iterator5.f();
|
|
4707
4712
|
}
|
|
4708
4713
|
}
|
|
4709
|
-
function parseBetween(view, from_, to_) {
|
|
4714
|
+
function parseBetween(view, from_, to_, addedNodes) {
|
|
4710
4715
|
var _view$docView$parseRa = view.docView.parseRange(from_, to_),
|
|
4711
4716
|
parent = _view$docView$parseRa.node,
|
|
4712
4717
|
fromOffset = _view$docView$parseRa.fromOffset,
|
|
@@ -4749,7 +4754,7 @@ function parseBetween(view, from_, to_) {
|
|
|
4749
4754
|
to: toOffset,
|
|
4750
4755
|
preserveWhitespace: $from.parent.type.whitespace == "pre" ? "full" : true,
|
|
4751
4756
|
findPositions: find,
|
|
4752
|
-
ruleFromNode: ruleFromNode,
|
|
4757
|
+
ruleFromNode: ruleFromNode(addedNodes),
|
|
4753
4758
|
context: $from
|
|
4754
4759
|
});
|
|
4755
4760
|
if (find && find[0].pos != null) {
|
|
@@ -4768,29 +4773,31 @@ function parseBetween(view, from_, to_) {
|
|
|
4768
4773
|
to: to
|
|
4769
4774
|
};
|
|
4770
4775
|
}
|
|
4771
|
-
function ruleFromNode(
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
if (
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4776
|
+
var ruleFromNode = function ruleFromNode(added) {
|
|
4777
|
+
return function (dom) {
|
|
4778
|
+
var desc = dom.pmViewDesc;
|
|
4779
|
+
if (desc) {
|
|
4780
|
+
return desc.parseRule(added);
|
|
4781
|
+
} else if (dom.nodeName == "BR" && dom.parentNode) {
|
|
4782
|
+
if (safari && /^(ul|ol)$/i.test(dom.parentNode.nodeName)) {
|
|
4783
|
+
var skip = document.createElement("div");
|
|
4784
|
+
skip.appendChild(document.createElement("li"));
|
|
4785
|
+
return {
|
|
4786
|
+
skip: skip
|
|
4787
|
+
};
|
|
4788
|
+
} else if (dom.parentNode.lastChild == dom || safari && /^(tr|table)$/i.test(dom.parentNode.nodeName)) {
|
|
4789
|
+
return {
|
|
4790
|
+
ignore: true
|
|
4791
|
+
};
|
|
4792
|
+
}
|
|
4793
|
+
} else if (dom.nodeName == "IMG" && dom.getAttribute("mark-placeholder")) {
|
|
4783
4794
|
return {
|
|
4784
4795
|
ignore: true
|
|
4785
4796
|
};
|
|
4786
4797
|
}
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
};
|
|
4791
|
-
}
|
|
4792
|
-
return null;
|
|
4793
|
-
}
|
|
4798
|
+
return null;
|
|
4799
|
+
};
|
|
4800
|
+
};
|
|
4794
4801
|
var isInline = /^(a|abbr|acronym|b|bd[io]|big|br|button|cite|code|data(list)?|del|dfn|em|i|img|ins|kbd|label|map|mark|meter|output|q|ruby|s|samp|small|span|strong|su[bp]|time|u|tt|var)$/i;
|
|
4795
4802
|
function readDOMChange(view, from, to, typeOver, addedNodes) {
|
|
4796
4803
|
var compositionID = view.input.compositionPendingChanges || (view.composing ? view.input.compositionID : 0);
|
|
@@ -4814,7 +4821,7 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
|
|
|
4814
4821
|
from = $before.before(shared + 1);
|
|
4815
4822
|
to = view.state.doc.resolve(to).after(shared + 1);
|
|
4816
4823
|
var sel = view.state.selection;
|
|
4817
|
-
var parse = parseBetween(view, from, to);
|
|
4824
|
+
var parse = parseBetween(view, from, to, addedNodes);
|
|
4818
4825
|
var doc = view.state.doc,
|
|
4819
4826
|
compare = doc.slice(parse.from, parse.to);
|
|
4820
4827
|
var preferredPos, preferredSide;
|
package/dist/index.js
CHANGED
|
@@ -764,7 +764,7 @@ class ViewDesc {
|
|
|
764
764
|
// When parsing in-editor content (in domchange.js), we allow
|
|
765
765
|
// descriptions to determine the parse rules that should be used to
|
|
766
766
|
// parse them.
|
|
767
|
-
parseRule() { return null; }
|
|
767
|
+
parseRule(addedNodes) { return null; }
|
|
768
768
|
// Used by the editor's event handler to ignore events that come
|
|
769
769
|
// from certain descs.
|
|
770
770
|
stopEvent(event) { return false; }
|
|
@@ -1328,7 +1328,7 @@ class NodeViewDesc extends ViewDesc {
|
|
|
1328
1328
|
else
|
|
1329
1329
|
return new NodeViewDesc(parent, node, outerDeco, innerDeco, dom, contentDOM || null, nodeDOM);
|
|
1330
1330
|
}
|
|
1331
|
-
parseRule() {
|
|
1331
|
+
parseRule(addedNodes) {
|
|
1332
1332
|
// Experimental kludge to allow opt-in re-parsing of nodes
|
|
1333
1333
|
if (this.node.type.spec.reparseInView)
|
|
1334
1334
|
return null;
|
|
@@ -1356,8 +1356,14 @@ class NodeViewDesc extends ViewDesc {
|
|
|
1356
1356
|
break;
|
|
1357
1357
|
}
|
|
1358
1358
|
}
|
|
1359
|
-
if (!rule.contentElement)
|
|
1360
|
-
|
|
1359
|
+
if (!rule.contentElement) {
|
|
1360
|
+
let found = addedNodes &&
|
|
1361
|
+
addedNodes.find(n => n.nodeType == 1 && addedNodes.indexOf(n.parentNode) < 0 && this.dom.contains(n));
|
|
1362
|
+
if (found)
|
|
1363
|
+
rule.contentElement = found;
|
|
1364
|
+
else
|
|
1365
|
+
rule.getContent = () => Fragment.empty;
|
|
1366
|
+
}
|
|
1361
1367
|
}
|
|
1362
1368
|
return rule;
|
|
1363
1369
|
}
|
|
@@ -3911,8 +3917,8 @@ handlers.beforeinput = (view, _event) => {
|
|
|
3911
3917
|
// We should probably do more with beforeinput events, but support
|
|
3912
3918
|
// is so spotty that I'm still waiting to see where they are going.
|
|
3913
3919
|
// Very specific hack to deal with backspace sometimes failing on
|
|
3914
|
-
// Chrome Android when after an uneditable node.
|
|
3915
|
-
if (
|
|
3920
|
+
// Chrome and Firefox Android when after an uneditable node.
|
|
3921
|
+
if (android && event.inputType == "deleteContentBackward") {
|
|
3916
3922
|
view.domObserver.flushSoon();
|
|
3917
3923
|
let { domChangeCount } = view.input;
|
|
3918
3924
|
setTimeout(() => {
|
|
@@ -5016,7 +5022,7 @@ function fixUpBadSafariComposition(view, addedNodes) {
|
|
|
5016
5022
|
// that the DOM represents. If any changes came in in the meantime,
|
|
5017
5023
|
// the modification is mapped over those before it is applied, in
|
|
5018
5024
|
// readDOMChange.
|
|
5019
|
-
function parseBetween(view, from_, to_) {
|
|
5025
|
+
function parseBetween(view, from_, to_, addedNodes) {
|
|
5020
5026
|
let { node: parent, fromOffset, toOffset, from, to } = view.docView.parseRange(from_, to_);
|
|
5021
5027
|
let domSel = view.domSelectionRange();
|
|
5022
5028
|
let find;
|
|
@@ -5050,7 +5056,7 @@ function parseBetween(view, from_, to_) {
|
|
|
5050
5056
|
to: toOffset,
|
|
5051
5057
|
preserveWhitespace: $from.parent.type.whitespace == "pre" ? "full" : true,
|
|
5052
5058
|
findPositions: find,
|
|
5053
|
-
ruleFromNode,
|
|
5059
|
+
ruleFromNode: ruleFromNode(addedNodes),
|
|
5054
5060
|
context: $from
|
|
5055
5061
|
});
|
|
5056
5062
|
if (find && find[0].pos != null) {
|
|
@@ -5061,10 +5067,10 @@ function parseBetween(view, from_, to_) {
|
|
|
5061
5067
|
}
|
|
5062
5068
|
return { doc, sel, from, to };
|
|
5063
5069
|
}
|
|
5064
|
-
|
|
5070
|
+
const ruleFromNode = (added) => (dom) => {
|
|
5065
5071
|
let desc = dom.pmViewDesc;
|
|
5066
5072
|
if (desc) {
|
|
5067
|
-
return desc.parseRule();
|
|
5073
|
+
return desc.parseRule(added);
|
|
5068
5074
|
}
|
|
5069
5075
|
else if (dom.nodeName == "BR" && dom.parentNode) {
|
|
5070
5076
|
// Safari replaces the list item or table cell with a BR
|
|
@@ -5083,7 +5089,7 @@ function ruleFromNode(dom) {
|
|
|
5083
5089
|
return { ignore: true };
|
|
5084
5090
|
}
|
|
5085
5091
|
return null;
|
|
5086
|
-
}
|
|
5092
|
+
};
|
|
5087
5093
|
const isInline = /^(a|abbr|acronym|b|bd[io]|big|br|button|cite|code|data(list)?|del|dfn|em|i|img|ins|kbd|label|map|mark|meter|output|q|ruby|s|samp|small|span|strong|su[bp]|time|u|tt|var)$/i;
|
|
5088
5094
|
function readDOMChange(view, from, to, typeOver, addedNodes) {
|
|
5089
5095
|
let compositionID = view.input.compositionPendingChanges || (view.composing ? view.input.compositionID : 0);
|
|
@@ -5112,7 +5118,7 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
|
|
|
5112
5118
|
from = $before.before(shared + 1);
|
|
5113
5119
|
to = view.state.doc.resolve(to).after(shared + 1);
|
|
5114
5120
|
let sel = view.state.selection;
|
|
5115
|
-
let parse = parseBetween(view, from, to);
|
|
5121
|
+
let parse = parseBetween(view, from, to, addedNodes);
|
|
5116
5122
|
let doc = view.state.doc, compare = doc.slice(parse.from, parse.to);
|
|
5117
5123
|
let preferredPos, preferredSide;
|
|
5118
5124
|
// Prefer anchoring to end when Backspace is pressed
|
package/package.json
CHANGED
package/src/domchange.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {EditorView} from "./index"
|
|
|
12
12
|
// the modification is mapped over those before it is applied, in
|
|
13
13
|
// readDOMChange.
|
|
14
14
|
|
|
15
|
-
function parseBetween(view: EditorView, from_: number, to_: number) {
|
|
15
|
+
function parseBetween(view: EditorView, from_: number, to_: number, addedNodes: readonly DOMNode[]) {
|
|
16
16
|
let {node: parent, fromOffset, toOffset, from, to} = view.docView.parseRange(from_, to_)
|
|
17
17
|
|
|
18
18
|
let domSel = view.domSelectionRange()
|
|
@@ -44,7 +44,7 @@ function parseBetween(view: EditorView, from_: number, to_: number) {
|
|
|
44
44
|
to: toOffset,
|
|
45
45
|
preserveWhitespace: $from.parent.type.whitespace == "pre" ? "full" : true,
|
|
46
46
|
findPositions: find,
|
|
47
|
-
ruleFromNode,
|
|
47
|
+
ruleFromNode: ruleFromNode(addedNodes),
|
|
48
48
|
context: $from
|
|
49
49
|
})
|
|
50
50
|
if (find && find[0].pos != null) {
|
|
@@ -55,10 +55,10 @@ function parseBetween(view: EditorView, from_: number, to_: number) {
|
|
|
55
55
|
return {doc, sel, from, to}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
const ruleFromNode = (added: readonly DOMNode[]) => (dom: DOMNode): Omit<TagParseRule, "tag"> | null => {
|
|
59
59
|
let desc = dom.pmViewDesc
|
|
60
60
|
if (desc) {
|
|
61
|
-
return desc.parseRule()
|
|
61
|
+
return desc.parseRule(added)
|
|
62
62
|
} else if (dom.nodeName == "BR" && dom.parentNode) {
|
|
63
63
|
// Safari replaces the list item or table cell with a BR
|
|
64
64
|
// directly in the list node (?!) if you delete the last
|
|
@@ -105,7 +105,7 @@ export function readDOMChange(view: EditorView, from: number, to: number, typeOv
|
|
|
105
105
|
to = view.state.doc.resolve(to).after(shared + 1)
|
|
106
106
|
|
|
107
107
|
let sel = view.state.selection
|
|
108
|
-
let parse = parseBetween(view, from, to)
|
|
108
|
+
let parse = parseBetween(view, from, to, addedNodes)
|
|
109
109
|
|
|
110
110
|
let doc = view.state.doc, compare = doc.slice(parse.from, parse.to)
|
|
111
111
|
let preferredPos, preferredSide: "start" | "end"
|
package/src/input.ts
CHANGED
|
@@ -864,8 +864,8 @@ handlers.beforeinput = (view, _event: Event) => {
|
|
|
864
864
|
// is so spotty that I'm still waiting to see where they are going.
|
|
865
865
|
|
|
866
866
|
// Very specific hack to deal with backspace sometimes failing on
|
|
867
|
-
// Chrome Android when after an uneditable node.
|
|
868
|
-
if (browser.
|
|
867
|
+
// Chrome and Firefox Android when after an uneditable node.
|
|
868
|
+
if (browser.android && event.inputType == "deleteContentBackward") {
|
|
869
869
|
view.domObserver.flushSoon()
|
|
870
870
|
let {domChangeCount} = view.input
|
|
871
871
|
setTimeout(() => {
|
package/src/viewdesc.ts
CHANGED
|
@@ -167,7 +167,7 @@ export class ViewDesc {
|
|
|
167
167
|
// When parsing in-editor content (in domchange.js), we allow
|
|
168
168
|
// descriptions to determine the parse rules that should be used to
|
|
169
169
|
// parse them.
|
|
170
|
-
parseRule(): Omit<TagParseRule, "tag"> | null { return null }
|
|
170
|
+
parseRule(addedNodes?: readonly DOMNode[]): Omit<TagParseRule, "tag"> | null { return null }
|
|
171
171
|
|
|
172
172
|
// Used by the editor's event handler to ignore events that come
|
|
173
173
|
// from certain descs.
|
|
@@ -727,7 +727,7 @@ export class NodeViewDesc extends ViewDesc {
|
|
|
727
727
|
return new NodeViewDesc(parent, node, outerDeco, innerDeco, dom, contentDOM || null, nodeDOM)
|
|
728
728
|
}
|
|
729
729
|
|
|
730
|
-
parseRule() {
|
|
730
|
+
parseRule(addedNodes?: readonly DOMNode[]) {
|
|
731
731
|
// Experimental kludge to allow opt-in re-parsing of nodes
|
|
732
732
|
if (this.node.type.spec.reparseInView) return null
|
|
733
733
|
// FIXME the assumption that this can always return the current
|
|
@@ -751,7 +751,14 @@ export class NodeViewDesc extends ViewDesc {
|
|
|
751
751
|
break
|
|
752
752
|
}
|
|
753
753
|
}
|
|
754
|
-
if (!rule.contentElement)
|
|
754
|
+
if (!rule.contentElement) {
|
|
755
|
+
let found = addedNodes &&
|
|
756
|
+
addedNodes.find(n => n.nodeType == 1 && addedNodes.indexOf(n.parentNode!) < 0 && this.dom.contains(n))
|
|
757
|
+
if (found)
|
|
758
|
+
rule.contentElement = found as HTMLElement
|
|
759
|
+
else
|
|
760
|
+
rule.getContent = () => Fragment.empty
|
|
761
|
+
}
|
|
755
762
|
}
|
|
756
763
|
return rule
|
|
757
764
|
}
|
package/src/#clipboard.ts#
DELETED
|
@@ -1,275 +0,0 @@
|
|
|
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
|
-
}
|