prosemirror-view 1.42.1 → 1.42.3
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 +18 -0
- package/dist/index.cjs +51 -34
- package/dist/index.js +34 -15
- package/package.json +1 -1
- package/src/clipboard.ts +7 -2
- package/src/domchange.ts +5 -5
- package/src/input.ts +2 -2
- package/src/viewdesc.ts +12 -4
- package/src/#clipboard.ts# +0 -275
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
## 1.42.3 (2026-08-25)
|
|
2
|
+
|
|
3
|
+
### Bug fixes
|
|
4
|
+
|
|
5
|
+
Don't set `contenteditable=false` on widgets that already have a `contenteditable` attribute.
|
|
6
|
+
|
|
7
|
+
Fix a bug that broke pasting when there was a default trusted type policy defined without a `createHTML` function.
|
|
8
|
+
|
|
9
|
+
Run attribute validators on attributes provided via slice context in clipboard content. This closes an XSS vulnerability (found by Pedro Paniago).
|
|
10
|
+
|
|
11
|
+
## 1.42.2 (2026-07-24)
|
|
12
|
+
|
|
13
|
+
### Bug fixes
|
|
14
|
+
|
|
15
|
+
Enable a workaround for backspace not taking effect on Android for Firefox as well as Chrome (for which it already kicked in).
|
|
16
|
+
|
|
17
|
+
Work around the situation where Chrome/Webkit replace a node's content DOM with a completely different element.
|
|
18
|
+
|
|
1
19
|
## 1.42.1 (2026-07-11)
|
|
2
20
|
|
|
3
21
|
### 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
|
}, {
|
|
@@ -1105,7 +1105,7 @@ var WidgetViewDesc = function (_ViewDesc) {
|
|
|
1105
1105
|
wrap.appendChild(dom);
|
|
1106
1106
|
dom = wrap;
|
|
1107
1107
|
}
|
|
1108
|
-
dom.contentEditable = "false";
|
|
1108
|
+
if (!dom.hasAttribute("contenteditable")) dom.contentEditable = "false";
|
|
1109
1109
|
dom.classList.add("ProseMirror-widget");
|
|
1110
1110
|
}
|
|
1111
1111
|
_this = _super.call(this, parent, [], dom, null);
|
|
@@ -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
|
}
|
|
@@ -2847,11 +2852,16 @@ var _policy = null;
|
|
|
2847
2852
|
function maybeWrapTrusted(html) {
|
|
2848
2853
|
var trustedTypes = window.trustedTypes;
|
|
2849
2854
|
if (!trustedTypes) return html;
|
|
2850
|
-
if (!_policy)
|
|
2851
|
-
|
|
2852
|
-
return
|
|
2853
|
-
}
|
|
2854
|
-
|
|
2855
|
+
if (!_policy) {
|
|
2856
|
+
if (_policy = trustedTypes.defaultPolicy) try {
|
|
2857
|
+
return _policy.createHTML(html);
|
|
2858
|
+
} catch (_a) {}
|
|
2859
|
+
_policy = trustedTypes.createPolicy("ProseMirrorClipboard", {
|
|
2860
|
+
createHTML: function createHTML(s) {
|
|
2861
|
+
return s;
|
|
2862
|
+
}
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2855
2865
|
return _policy.createHTML(html);
|
|
2856
2866
|
}
|
|
2857
2867
|
function readHTML(html) {
|
|
@@ -2902,6 +2912,11 @@ function addContext(slice, context) {
|
|
|
2902
2912
|
for (var i = array.length - 2; i >= 0; i -= 2) {
|
|
2903
2913
|
var type = schema.nodes[array[i]];
|
|
2904
2914
|
if (!type || type.hasRequiredAttrs()) break;
|
|
2915
|
+
try {
|
|
2916
|
+
type.checkAttrs(array[i + 1]);
|
|
2917
|
+
} catch (e) {
|
|
2918
|
+
break;
|
|
2919
|
+
}
|
|
2905
2920
|
content = prosemirrorModel.Fragment.from(type.create(array[i + 1], content));
|
|
2906
2921
|
openStart++;
|
|
2907
2922
|
openEnd++;
|
|
@@ -3669,7 +3684,7 @@ handlers.blur = function (view, _event) {
|
|
|
3669
3684
|
};
|
|
3670
3685
|
handlers.beforeinput = function (view, _event) {
|
|
3671
3686
|
var event = _event;
|
|
3672
|
-
if (
|
|
3687
|
+
if (android && event.inputType == "deleteContentBackward") {
|
|
3673
3688
|
view.domObserver.flushSoon();
|
|
3674
3689
|
var domChangeCount = view.input.domChangeCount;
|
|
3675
3690
|
setTimeout(function () {
|
|
@@ -4706,7 +4721,7 @@ function fixUpBadSafariComposition(view, addedNodes) {
|
|
|
4706
4721
|
_iterator5.f();
|
|
4707
4722
|
}
|
|
4708
4723
|
}
|
|
4709
|
-
function parseBetween(view, from_, to_) {
|
|
4724
|
+
function parseBetween(view, from_, to_, addedNodes) {
|
|
4710
4725
|
var _view$docView$parseRa = view.docView.parseRange(from_, to_),
|
|
4711
4726
|
parent = _view$docView$parseRa.node,
|
|
4712
4727
|
fromOffset = _view$docView$parseRa.fromOffset,
|
|
@@ -4749,7 +4764,7 @@ function parseBetween(view, from_, to_) {
|
|
|
4749
4764
|
to: toOffset,
|
|
4750
4765
|
preserveWhitespace: $from.parent.type.whitespace == "pre" ? "full" : true,
|
|
4751
4766
|
findPositions: find,
|
|
4752
|
-
ruleFromNode: ruleFromNode,
|
|
4767
|
+
ruleFromNode: ruleFromNode(addedNodes),
|
|
4753
4768
|
context: $from
|
|
4754
4769
|
});
|
|
4755
4770
|
if (find && find[0].pos != null) {
|
|
@@ -4768,29 +4783,31 @@ function parseBetween(view, from_, to_) {
|
|
|
4768
4783
|
to: to
|
|
4769
4784
|
};
|
|
4770
4785
|
}
|
|
4771
|
-
function ruleFromNode(
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
if (
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4786
|
+
var ruleFromNode = function ruleFromNode(added) {
|
|
4787
|
+
return function (dom) {
|
|
4788
|
+
var desc = dom.pmViewDesc;
|
|
4789
|
+
if (desc) {
|
|
4790
|
+
return desc.parseRule(added);
|
|
4791
|
+
} else if (dom.nodeName == "BR" && dom.parentNode) {
|
|
4792
|
+
if (safari && /^(ul|ol)$/i.test(dom.parentNode.nodeName)) {
|
|
4793
|
+
var skip = document.createElement("div");
|
|
4794
|
+
skip.appendChild(document.createElement("li"));
|
|
4795
|
+
return {
|
|
4796
|
+
skip: skip
|
|
4797
|
+
};
|
|
4798
|
+
} else if (dom.parentNode.lastChild == dom || safari && /^(tr|table)$/i.test(dom.parentNode.nodeName)) {
|
|
4799
|
+
return {
|
|
4800
|
+
ignore: true
|
|
4801
|
+
};
|
|
4802
|
+
}
|
|
4803
|
+
} else if (dom.nodeName == "IMG" && dom.getAttribute("mark-placeholder")) {
|
|
4783
4804
|
return {
|
|
4784
4805
|
ignore: true
|
|
4785
4806
|
};
|
|
4786
4807
|
}
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
};
|
|
4791
|
-
}
|
|
4792
|
-
return null;
|
|
4793
|
-
}
|
|
4808
|
+
return null;
|
|
4809
|
+
};
|
|
4810
|
+
};
|
|
4794
4811
|
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
4812
|
function readDOMChange(view, from, to, typeOver, addedNodes) {
|
|
4796
4813
|
var compositionID = view.input.compositionPendingChanges || (view.composing ? view.input.compositionID : 0);
|
|
@@ -4814,7 +4831,7 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
|
|
|
4814
4831
|
from = $before.before(shared + 1);
|
|
4815
4832
|
to = view.state.doc.resolve(to).after(shared + 1);
|
|
4816
4833
|
var sel = view.state.selection;
|
|
4817
|
-
var parse = parseBetween(view, from, to);
|
|
4834
|
+
var parse = parseBetween(view, from, to, addedNodes);
|
|
4818
4835
|
var doc = view.state.doc,
|
|
4819
4836
|
compare = doc.slice(parse.from, parse.to);
|
|
4820
4837
|
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; }
|
|
@@ -1169,7 +1169,8 @@ class WidgetViewDesc extends ViewDesc {
|
|
|
1169
1169
|
wrap.appendChild(dom);
|
|
1170
1170
|
dom = wrap;
|
|
1171
1171
|
}
|
|
1172
|
-
dom.
|
|
1172
|
+
if (!dom.hasAttribute("contenteditable"))
|
|
1173
|
+
dom.contentEditable = "false";
|
|
1173
1174
|
dom.classList.add("ProseMirror-widget");
|
|
1174
1175
|
}
|
|
1175
1176
|
super(parent, [], dom, null);
|
|
@@ -1328,7 +1329,7 @@ class NodeViewDesc extends ViewDesc {
|
|
|
1328
1329
|
else
|
|
1329
1330
|
return new NodeViewDesc(parent, node, outerDeco, innerDeco, dom, contentDOM || null, nodeDOM);
|
|
1330
1331
|
}
|
|
1331
|
-
parseRule() {
|
|
1332
|
+
parseRule(addedNodes) {
|
|
1332
1333
|
// Experimental kludge to allow opt-in re-parsing of nodes
|
|
1333
1334
|
if (this.node.type.spec.reparseInView)
|
|
1334
1335
|
return null;
|
|
@@ -1356,8 +1357,14 @@ class NodeViewDesc extends ViewDesc {
|
|
|
1356
1357
|
break;
|
|
1357
1358
|
}
|
|
1358
1359
|
}
|
|
1359
|
-
if (!rule.contentElement)
|
|
1360
|
-
|
|
1360
|
+
if (!rule.contentElement) {
|
|
1361
|
+
let found = addedNodes &&
|
|
1362
|
+
addedNodes.find(n => n.nodeType == 1 && addedNodes.indexOf(n.parentNode) < 0 && this.dom.contains(n));
|
|
1363
|
+
if (found)
|
|
1364
|
+
rule.contentElement = found;
|
|
1365
|
+
else
|
|
1366
|
+
rule.getContent = () => Fragment.empty;
|
|
1367
|
+
}
|
|
1361
1368
|
}
|
|
1362
1369
|
return rule;
|
|
1363
1370
|
}
|
|
@@ -3011,8 +3018,14 @@ function maybeWrapTrusted(html) {
|
|
|
3011
3018
|
// With the require-trusted-types-for CSP, Chrome will block
|
|
3012
3019
|
// innerHTML, even on a detached document. This wraps the string in
|
|
3013
3020
|
// a way that makes the browser allow us to use its parser again.
|
|
3014
|
-
if (!_policy)
|
|
3015
|
-
_policy = trustedTypes.defaultPolicy
|
|
3021
|
+
if (!_policy) {
|
|
3022
|
+
if (_policy = trustedTypes.defaultPolicy)
|
|
3023
|
+
try {
|
|
3024
|
+
return _policy.createHTML(html);
|
|
3025
|
+
}
|
|
3026
|
+
catch (_a) { }
|
|
3027
|
+
_policy = trustedTypes.createPolicy("ProseMirrorClipboard", { createHTML: (s) => s });
|
|
3028
|
+
}
|
|
3016
3029
|
return _policy.createHTML(html);
|
|
3017
3030
|
}
|
|
3018
3031
|
function readHTML(html) {
|
|
@@ -3069,6 +3082,12 @@ function addContext(slice, context) {
|
|
|
3069
3082
|
let type = schema.nodes[array[i]];
|
|
3070
3083
|
if (!type || type.hasRequiredAttrs())
|
|
3071
3084
|
break;
|
|
3085
|
+
try {
|
|
3086
|
+
type.checkAttrs(array[i + 1]);
|
|
3087
|
+
}
|
|
3088
|
+
catch (e) {
|
|
3089
|
+
break;
|
|
3090
|
+
}
|
|
3072
3091
|
content = Fragment.from(type.create(array[i + 1], content));
|
|
3073
3092
|
openStart++;
|
|
3074
3093
|
openEnd++;
|
|
@@ -3911,8 +3930,8 @@ handlers.beforeinput = (view, _event) => {
|
|
|
3911
3930
|
// We should probably do more with beforeinput events, but support
|
|
3912
3931
|
// is so spotty that I'm still waiting to see where they are going.
|
|
3913
3932
|
// Very specific hack to deal with backspace sometimes failing on
|
|
3914
|
-
// Chrome Android when after an uneditable node.
|
|
3915
|
-
if (
|
|
3933
|
+
// Chrome and Firefox Android when after an uneditable node.
|
|
3934
|
+
if (android && event.inputType == "deleteContentBackward") {
|
|
3916
3935
|
view.domObserver.flushSoon();
|
|
3917
3936
|
let { domChangeCount } = view.input;
|
|
3918
3937
|
setTimeout(() => {
|
|
@@ -5016,7 +5035,7 @@ function fixUpBadSafariComposition(view, addedNodes) {
|
|
|
5016
5035
|
// that the DOM represents. If any changes came in in the meantime,
|
|
5017
5036
|
// the modification is mapped over those before it is applied, in
|
|
5018
5037
|
// readDOMChange.
|
|
5019
|
-
function parseBetween(view, from_, to_) {
|
|
5038
|
+
function parseBetween(view, from_, to_, addedNodes) {
|
|
5020
5039
|
let { node: parent, fromOffset, toOffset, from, to } = view.docView.parseRange(from_, to_);
|
|
5021
5040
|
let domSel = view.domSelectionRange();
|
|
5022
5041
|
let find;
|
|
@@ -5050,7 +5069,7 @@ function parseBetween(view, from_, to_) {
|
|
|
5050
5069
|
to: toOffset,
|
|
5051
5070
|
preserveWhitespace: $from.parent.type.whitespace == "pre" ? "full" : true,
|
|
5052
5071
|
findPositions: find,
|
|
5053
|
-
ruleFromNode,
|
|
5072
|
+
ruleFromNode: ruleFromNode(addedNodes),
|
|
5054
5073
|
context: $from
|
|
5055
5074
|
});
|
|
5056
5075
|
if (find && find[0].pos != null) {
|
|
@@ -5061,10 +5080,10 @@ function parseBetween(view, from_, to_) {
|
|
|
5061
5080
|
}
|
|
5062
5081
|
return { doc, sel, from, to };
|
|
5063
5082
|
}
|
|
5064
|
-
|
|
5083
|
+
const ruleFromNode = (added) => (dom) => {
|
|
5065
5084
|
let desc = dom.pmViewDesc;
|
|
5066
5085
|
if (desc) {
|
|
5067
|
-
return desc.parseRule();
|
|
5086
|
+
return desc.parseRule(added);
|
|
5068
5087
|
}
|
|
5069
5088
|
else if (dom.nodeName == "BR" && dom.parentNode) {
|
|
5070
5089
|
// Safari replaces the list item or table cell with a BR
|
|
@@ -5083,7 +5102,7 @@ function ruleFromNode(dom) {
|
|
|
5083
5102
|
return { ignore: true };
|
|
5084
5103
|
}
|
|
5085
5104
|
return null;
|
|
5086
|
-
}
|
|
5105
|
+
};
|
|
5087
5106
|
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
5107
|
function readDOMChange(view, from, to, typeOver, addedNodes) {
|
|
5089
5108
|
let compositionID = view.input.compositionPendingChanges || (view.composing ? view.input.compositionID : 0);
|
|
@@ -5112,7 +5131,7 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
|
|
|
5112
5131
|
from = $before.before(shared + 1);
|
|
5113
5132
|
to = view.state.doc.resolve(to).after(shared + 1);
|
|
5114
5133
|
let sel = view.state.selection;
|
|
5115
|
-
let parse = parseBetween(view, from, to);
|
|
5134
|
+
let parse = parseBetween(view, from, to, addedNodes);
|
|
5116
5135
|
let doc = view.state.doc, compare = doc.slice(parse.from, parse.to);
|
|
5117
5136
|
let preferredPos, preferredSide;
|
|
5118
5137
|
// Prefer anchoring to end when Backspace is pressed
|
package/package.json
CHANGED
package/src/clipboard.ts
CHANGED
|
@@ -215,8 +215,11 @@ function maybeWrapTrusted(html: string): string {
|
|
|
215
215
|
// With the require-trusted-types-for CSP, Chrome will block
|
|
216
216
|
// innerHTML, even on a detached document. This wraps the string in
|
|
217
217
|
// a way that makes the browser allow us to use its parser again.
|
|
218
|
-
if (!_policy)
|
|
219
|
-
_policy = trustedTypes.defaultPolicy
|
|
218
|
+
if (!_policy) {
|
|
219
|
+
if (_policy = trustedTypes.defaultPolicy)
|
|
220
|
+
try { return _policy.createHTML(html) } catch {}
|
|
221
|
+
_policy = trustedTypes.createPolicy("ProseMirrorClipboard", {createHTML: (s: string) => s})
|
|
222
|
+
}
|
|
220
223
|
return _policy.createHTML(html)
|
|
221
224
|
}
|
|
222
225
|
|
|
@@ -267,6 +270,8 @@ function addContext(slice: Slice, context: string) {
|
|
|
267
270
|
for (let i = array.length - 2; i >= 0; i -= 2) {
|
|
268
271
|
let type = schema.nodes[array[i]]
|
|
269
272
|
if (!type || type.hasRequiredAttrs()) break
|
|
273
|
+
try { type.checkAttrs(array[i + 1]) }
|
|
274
|
+
catch(e) { break }
|
|
270
275
|
content = Fragment.from(type.create(array[i + 1], content))
|
|
271
276
|
openStart++; openEnd++
|
|
272
277
|
}
|
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.
|
|
@@ -555,7 +555,8 @@ class WidgetViewDesc extends ViewDesc {
|
|
|
555
555
|
wrap.appendChild(dom)
|
|
556
556
|
dom = wrap
|
|
557
557
|
}
|
|
558
|
-
|
|
558
|
+
if (!(dom as HTMLElement).hasAttribute("contenteditable"))
|
|
559
|
+
(dom as HTMLElement).contentEditable = "false"
|
|
559
560
|
;(dom as HTMLElement).classList.add("ProseMirror-widget")
|
|
560
561
|
}
|
|
561
562
|
super(parent, [], dom, null)
|
|
@@ -727,7 +728,7 @@ export class NodeViewDesc extends ViewDesc {
|
|
|
727
728
|
return new NodeViewDesc(parent, node, outerDeco, innerDeco, dom, contentDOM || null, nodeDOM)
|
|
728
729
|
}
|
|
729
730
|
|
|
730
|
-
parseRule() {
|
|
731
|
+
parseRule(addedNodes?: readonly DOMNode[]) {
|
|
731
732
|
// Experimental kludge to allow opt-in re-parsing of nodes
|
|
732
733
|
if (this.node.type.spec.reparseInView) return null
|
|
733
734
|
// FIXME the assumption that this can always return the current
|
|
@@ -751,7 +752,14 @@ export class NodeViewDesc extends ViewDesc {
|
|
|
751
752
|
break
|
|
752
753
|
}
|
|
753
754
|
}
|
|
754
|
-
if (!rule.contentElement)
|
|
755
|
+
if (!rule.contentElement) {
|
|
756
|
+
let found = addedNodes &&
|
|
757
|
+
addedNodes.find(n => n.nodeType == 1 && addedNodes.indexOf(n.parentNode!) < 0 && this.dom.contains(n))
|
|
758
|
+
if (found)
|
|
759
|
+
rule.contentElement = found as HTMLElement
|
|
760
|
+
else
|
|
761
|
+
rule.getContent = () => Fragment.empty
|
|
762
|
+
}
|
|
755
763
|
}
|
|
756
764
|
return rule
|
|
757
765
|
}
|
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
|
-
}
|