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 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) rule.getContent = function () {
1311
- return prosemirrorModel.Fragment.empty;
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 (chrome && android && event.inputType == "deleteContentBackward") {
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(dom) {
4772
- var desc = dom.pmViewDesc;
4773
- if (desc) {
4774
- return desc.parseRule();
4775
- } else if (dom.nodeName == "BR" && dom.parentNode) {
4776
- if (safari && /^(ul|ol)$/i.test(dom.parentNode.nodeName)) {
4777
- var skip = document.createElement("div");
4778
- skip.appendChild(document.createElement("li"));
4779
- return {
4780
- skip: skip
4781
- };
4782
- } else if (dom.parentNode.lastChild == dom || safari && /^(tr|table)$/i.test(dom.parentNode.nodeName)) {
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
- } else if (dom.nodeName == "IMG" && dom.getAttribute("mark-placeholder")) {
4788
- return {
4789
- ignore: true
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
- rule.getContent = () => Fragment.empty;
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 (chrome && android && event.inputType == "deleteContentBackward") {
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
- function ruleFromNode(dom) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prosemirror-view",
3
- "version": "1.42.1",
3
+ "version": "1.42.2",
4
4
  "description": "ProseMirror's view component",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
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
- function ruleFromNode(dom: DOMNode): Omit<TagParseRule, "tag"> | null {
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.chrome && browser.android && event.inputType == "deleteContentBackward") {
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) rule.getContent = () => Fragment.empty
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
  }
@@ -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
- }