@b9g/revise 0.1.1 → 0.1.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/README.md +67 -2
- package/_subseq.d.ts +18 -0
- package/contentarea.cjs +447 -419
- package/contentarea.cjs.map +1 -1
- package/contentarea.d.ts +57 -57
- package/contentarea.js +447 -419
- package/contentarea.js.map +1 -1
- package/edit.cjs +470 -199
- package/edit.cjs.map +1 -1
- package/edit.d.ts +71 -26
- package/edit.js +470 -199
- package/edit.js.map +1 -1
- package/history.cjs +3 -0
- package/history.cjs.map +1 -1
- package/history.d.ts +1 -1
- package/history.js +3 -0
- package/history.js.map +1 -1
- package/keyer.cjs +38 -15
- package/keyer.cjs.map +1 -1
- package/keyer.d.ts +2 -2
- package/keyer.js +38 -15
- package/keyer.js.map +1 -1
- package/package.json +16 -42
- package/subseq.cjs +0 -251
- package/subseq.cjs.map +0 -1
- package/subseq.d.ts +0 -62
- package/subseq.js +0 -248
- package/subseq.js.map +0 -1
package/contentarea.js
CHANGED
|
@@ -1,497 +1,530 @@
|
|
|
1
1
|
/// <reference types="contentarea.d.ts" />
|
|
2
2
|
import { Edit } from './edit.js';
|
|
3
|
-
import './subseq.js';
|
|
4
3
|
|
|
5
4
|
/// <reference lib="dom" />
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
constructor(offset) {
|
|
20
|
-
this.flags = 0;
|
|
21
|
-
this.size = 0;
|
|
22
|
-
this.offset = offset;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
/********************************************/
|
|
26
|
-
/*** ContentAreaElement symbol properties ***/
|
|
27
|
-
/********************************************/
|
|
28
|
-
const $slot = Symbol.for("revise$slot");
|
|
29
|
-
const $cache = Symbol.for("revise$cache");
|
|
30
|
-
const $value = Symbol.for("revise$value");
|
|
31
|
-
const $observer = Symbol.for("revise$observer");
|
|
32
|
-
const $selectionStart = Symbol.for("revise$selectionStart");
|
|
33
|
-
const $onselectionchange = Symbol.for("revise$onselectionchange");
|
|
34
|
-
const css = `:host {
|
|
35
|
-
display: contents;
|
|
36
|
-
white-space: pre-wrap;
|
|
37
|
-
white-space: break-spaces;
|
|
38
|
-
overflow-wrap: break-word;
|
|
39
|
-
}`;
|
|
5
|
+
/***************************************************/
|
|
6
|
+
/*** ContentAreaElement private property symbols ***/
|
|
7
|
+
/***************************************************/
|
|
8
|
+
const _cache = Symbol.for("ContentArea._cache");
|
|
9
|
+
const _observer = Symbol.for("ContentArea._observer");
|
|
10
|
+
const _onselectionchange = Symbol.for("ContentArea._onselectionchange");
|
|
11
|
+
const _value = Symbol.for("ContentArea._value");
|
|
12
|
+
const _selectionRange = Symbol.for("ContentArea._selectionRange");
|
|
13
|
+
const _staleValue = Symbol.for("ContentArea._staleValue");
|
|
14
|
+
const _staleSelectionRange = Symbol.for("ContentArea._slateSelectionRange");
|
|
15
|
+
const _compositionBuffer = Symbol.for("ContentArea._compositionBuffer");
|
|
16
|
+
const _compositionStartValue = Symbol.for("ContentArea._compositionStartValue");
|
|
17
|
+
const _compositionSelectionRange = Symbol.for("ContentArea._compositionSelectionRange");
|
|
40
18
|
class ContentAreaElement extends HTMLElement {
|
|
41
19
|
constructor() {
|
|
42
20
|
super();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
slot.contentEditable = this.contentEditable;
|
|
51
|
-
shadow.appendChild(slot);
|
|
52
|
-
this[$slot] = slot;
|
|
53
|
-
}
|
|
54
|
-
this[$cache] = new Map();
|
|
55
|
-
this[$value] = "";
|
|
56
|
-
this[$observer] = new MutationObserver((records) => {
|
|
57
|
-
validate(this, null, records);
|
|
21
|
+
this[_cache] = new Map();
|
|
22
|
+
this[_observer] = new MutationObserver((records) => {
|
|
23
|
+
if (this[_compositionBuffer]) {
|
|
24
|
+
// Buffer mutations during composition but still process them to keep cache in sync
|
|
25
|
+
this[_compositionBuffer].push(...records);
|
|
26
|
+
}
|
|
27
|
+
validate(this, records);
|
|
58
28
|
});
|
|
59
|
-
this[
|
|
60
|
-
|
|
61
|
-
validate(this);
|
|
62
|
-
this[$selectionStart] = getSelectionRange(this, this[$cache]).selectionStart;
|
|
29
|
+
this[_onselectionchange] = () => {
|
|
30
|
+
this[_selectionRange] = getSelectionRange(this);
|
|
63
31
|
};
|
|
32
|
+
this[_value] = "";
|
|
33
|
+
this[_selectionRange] = { start: 0, end: 0, direction: "none" };
|
|
34
|
+
this[_staleValue] = undefined;
|
|
35
|
+
this[_staleSelectionRange] = undefined;
|
|
36
|
+
this[_compositionBuffer] = undefined;
|
|
37
|
+
this[_compositionStartValue] = undefined;
|
|
38
|
+
this[_compositionSelectionRange] = undefined;
|
|
64
39
|
}
|
|
65
40
|
/******************************/
|
|
66
41
|
/*** Custom Element methods ***/
|
|
67
42
|
/******************************/
|
|
68
|
-
static get observedAttributes() {
|
|
69
|
-
return ["contenteditable"];
|
|
70
|
-
}
|
|
71
43
|
connectedCallback() {
|
|
72
|
-
this[
|
|
44
|
+
this[_observer].observe(this, {
|
|
73
45
|
subtree: true,
|
|
74
46
|
childList: true,
|
|
75
47
|
characterData: true,
|
|
48
|
+
characterDataOldValue: true,
|
|
76
49
|
attributes: true,
|
|
50
|
+
attributeOldValue: true,
|
|
77
51
|
attributeFilter: [
|
|
78
52
|
"data-content",
|
|
79
|
-
|
|
80
|
-
"data-
|
|
53
|
+
// TODO: implement these attributes
|
|
54
|
+
//"data-contentbefore",
|
|
55
|
+
//"data-contentafter",
|
|
81
56
|
],
|
|
82
57
|
});
|
|
83
|
-
|
|
84
|
-
this[$selectionStart] = getSelectionRange(this, this[$cache]).selectionStart;
|
|
85
|
-
document.addEventListener("selectionchange", this[$onselectionchange],
|
|
58
|
+
document.addEventListener("selectionchange", this[_onselectionchange],
|
|
86
59
|
// We use capture in an attempt to run before other event listeners.
|
|
87
60
|
true);
|
|
61
|
+
validate(this);
|
|
62
|
+
this[_onselectionchange]();
|
|
63
|
+
// Composition event handling
|
|
64
|
+
let processCompositionTimeout;
|
|
65
|
+
this.addEventListener("compositionstart", () => {
|
|
66
|
+
clearTimeout(processCompositionTimeout); // Cancel pending commit
|
|
67
|
+
if (processCompositionTimeout == null) {
|
|
68
|
+
this[_compositionBuffer] = [];
|
|
69
|
+
this[_compositionStartValue] = this[_value];
|
|
70
|
+
this[_compositionSelectionRange] = { ...this[_selectionRange] };
|
|
71
|
+
}
|
|
72
|
+
processCompositionTimeout = undefined;
|
|
73
|
+
});
|
|
74
|
+
const processComposition = () => {
|
|
75
|
+
if (this[_compositionBuffer] &&
|
|
76
|
+
this[_compositionBuffer].length > 0 &&
|
|
77
|
+
this[_compositionStartValue] !== undefined &&
|
|
78
|
+
this[_compositionSelectionRange] !== undefined) {
|
|
79
|
+
const edit = Edit.diff(this[_compositionStartValue], this[_value], this[_compositionSelectionRange].start);
|
|
80
|
+
const ev = new ContentEvent("contentchange", {
|
|
81
|
+
detail: { edit, source: null, mutations: this[_compositionBuffer] }
|
|
82
|
+
});
|
|
83
|
+
this.dispatchEvent(ev);
|
|
84
|
+
this[_staleValue] = undefined;
|
|
85
|
+
this[_staleSelectionRange] = undefined;
|
|
86
|
+
}
|
|
87
|
+
this[_compositionBuffer] = undefined;
|
|
88
|
+
this[_compositionStartValue] = undefined;
|
|
89
|
+
this[_compositionSelectionRange] = undefined;
|
|
90
|
+
processCompositionTimeout = undefined;
|
|
91
|
+
};
|
|
92
|
+
this.addEventListener("compositionend", () => {
|
|
93
|
+
clearTimeout(processCompositionTimeout);
|
|
94
|
+
processCompositionTimeout = setTimeout(processComposition);
|
|
95
|
+
});
|
|
96
|
+
this.addEventListener("blur", () => {
|
|
97
|
+
clearTimeout(processCompositionTimeout);
|
|
98
|
+
processComposition();
|
|
99
|
+
});
|
|
100
|
+
this.addEventListener("keydown", (e) => {
|
|
101
|
+
if (e.key === "Escape" && this[_compositionBuffer]) {
|
|
102
|
+
clearTimeout(processCompositionTimeout);
|
|
103
|
+
processComposition();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
88
106
|
}
|
|
89
107
|
disconnectedCallback() {
|
|
90
|
-
this[
|
|
91
|
-
this[
|
|
92
|
-
this[
|
|
93
|
-
// JSDOM-based environments like Jest
|
|
94
|
-
// before calling the disconnectedCallback for some reason.
|
|
108
|
+
this[_cache].clear();
|
|
109
|
+
this[_value] = "";
|
|
110
|
+
this[_observer].disconnect();
|
|
111
|
+
// JSDOM-based environments like Jest sometimes make the global document
|
|
112
|
+
// null before calling the disconnectedCallback for some reason.
|
|
95
113
|
if (document) {
|
|
96
|
-
document.removeEventListener("selectionchange", this[
|
|
114
|
+
document.removeEventListener("selectionchange", this[_onselectionchange], true);
|
|
97
115
|
}
|
|
98
116
|
}
|
|
99
|
-
attributeChangedCallback(name) {
|
|
100
|
-
switch (name) {
|
|
101
|
-
case "contenteditable": {
|
|
102
|
-
const slot = this[$slot];
|
|
103
|
-
// We have to set slot.contentEditable to this.contentEditable because
|
|
104
|
-
// Chrome has trouble selecting elements across shadow DOM boundaries.
|
|
105
|
-
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1175930
|
|
106
|
-
// Chrome has additional issues with using the host element as a
|
|
107
|
-
// contenteditable element but this normalizes some of the behavior.
|
|
108
|
-
slot.contentEditable = this.contentEditable;
|
|
109
|
-
break;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
/***********************/
|
|
114
|
-
/*** Content methods ***/
|
|
115
|
-
/***********************/
|
|
116
117
|
get value() {
|
|
117
118
|
validate(this);
|
|
118
|
-
return this[
|
|
119
|
+
return this[_staleValue] == null ? this[_value] : this[_staleValue];
|
|
119
120
|
}
|
|
120
121
|
get selectionStart() {
|
|
121
122
|
validate(this);
|
|
122
|
-
|
|
123
|
+
const range = this[_staleSelectionRange] || this[_selectionRange];
|
|
124
|
+
return range.start;
|
|
123
125
|
}
|
|
124
|
-
set selectionStart(
|
|
126
|
+
set selectionStart(start) {
|
|
125
127
|
validate(this);
|
|
126
|
-
const
|
|
127
|
-
setSelectionRange(this,
|
|
128
|
+
const { end, direction } = getSelectionRange(this);
|
|
129
|
+
setSelectionRange(this, { start, end, direction });
|
|
128
130
|
}
|
|
129
131
|
get selectionEnd() {
|
|
130
132
|
validate(this);
|
|
131
|
-
|
|
133
|
+
const range = this[_staleSelectionRange] || this[_selectionRange];
|
|
134
|
+
return range.end;
|
|
132
135
|
}
|
|
133
|
-
set selectionEnd(
|
|
136
|
+
set selectionEnd(end) {
|
|
134
137
|
validate(this);
|
|
135
|
-
const
|
|
136
|
-
setSelectionRange(this,
|
|
138
|
+
const { start, direction } = getSelectionRange(this);
|
|
139
|
+
setSelectionRange(this, { start, end, direction });
|
|
137
140
|
}
|
|
138
141
|
get selectionDirection() {
|
|
139
142
|
validate(this);
|
|
140
|
-
|
|
143
|
+
const range = this[_staleSelectionRange] || this[_selectionRange];
|
|
144
|
+
return range.direction;
|
|
141
145
|
}
|
|
142
|
-
set selectionDirection(
|
|
146
|
+
set selectionDirection(direction) {
|
|
143
147
|
validate(this);
|
|
144
|
-
const
|
|
145
|
-
setSelectionRange(this,
|
|
148
|
+
const { start, end } = getSelectionRange(this);
|
|
149
|
+
setSelectionRange(this, { start, end, direction });
|
|
146
150
|
}
|
|
147
151
|
getSelectionRange() {
|
|
148
152
|
validate(this);
|
|
149
|
-
|
|
153
|
+
const range = this[_staleSelectionRange] || this[_selectionRange];
|
|
154
|
+
return { ...range };
|
|
150
155
|
}
|
|
151
|
-
setSelectionRange(
|
|
156
|
+
setSelectionRange(start, end, direction = "none") {
|
|
152
157
|
validate(this);
|
|
153
|
-
setSelectionRange(this,
|
|
158
|
+
setSelectionRange(this, { start, end, direction });
|
|
154
159
|
}
|
|
155
160
|
indexAt(node, offset) {
|
|
156
161
|
validate(this);
|
|
157
|
-
|
|
158
|
-
return indexAt(this, cache, node, offset);
|
|
162
|
+
return indexAt(this, node, offset);
|
|
159
163
|
}
|
|
160
164
|
nodeOffsetAt(index) {
|
|
161
165
|
validate(this);
|
|
162
|
-
|
|
163
|
-
return nodeOffsetAt(this, cache, index);
|
|
166
|
+
return nodeOffsetAt(this, index);
|
|
164
167
|
}
|
|
165
168
|
source(source) {
|
|
166
|
-
return validate(this,
|
|
169
|
+
return validate(this, this[_observer].takeRecords(), source);
|
|
167
170
|
}
|
|
168
171
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
"ARTICLE",
|
|
175
|
-
"ASIDE",
|
|
176
|
-
"BLOCKQUOTE",
|
|
177
|
-
"CAPTION",
|
|
178
|
-
"DETAILS",
|
|
179
|
-
"DIALOG",
|
|
180
|
-
"DD",
|
|
181
|
-
"DIV",
|
|
182
|
-
"DL",
|
|
183
|
-
"DT",
|
|
184
|
-
"FIELDSET",
|
|
185
|
-
"FIGCAPTION",
|
|
186
|
-
"FIGURE",
|
|
187
|
-
"FOOTER",
|
|
188
|
-
"FORM",
|
|
189
|
-
"H1",
|
|
190
|
-
"H2",
|
|
191
|
-
"H3",
|
|
192
|
-
"H4",
|
|
193
|
-
"H5",
|
|
194
|
-
"H6",
|
|
195
|
-
"HEADER",
|
|
196
|
-
"HGROUP",
|
|
197
|
-
"HR",
|
|
198
|
-
"LI",
|
|
199
|
-
"MAIN",
|
|
200
|
-
"NAV",
|
|
201
|
-
"OL",
|
|
202
|
-
"P",
|
|
203
|
-
"PRE",
|
|
204
|
-
"SECTION",
|
|
205
|
-
"TABLE",
|
|
206
|
-
"TR",
|
|
207
|
-
"UL",
|
|
208
|
-
]);
|
|
209
|
-
function validate(root, source = null, records) {
|
|
210
|
-
const cache = root[$cache];
|
|
211
|
-
// We use the existence of records to determine whether
|
|
212
|
-
// contentchange events should be fired synchronously.
|
|
213
|
-
let delay = false;
|
|
214
|
-
if (records === undefined) {
|
|
215
|
-
delay = true;
|
|
216
|
-
records = root[$observer].takeRecords();
|
|
217
|
-
}
|
|
218
|
-
if (!invalidate(root, cache, records)) {
|
|
219
|
-
return false;
|
|
172
|
+
const PreventDefaultSource = Symbol.for("ContentArea.PreventDefaultSource");
|
|
173
|
+
class ContentEvent extends CustomEvent {
|
|
174
|
+
constructor(typeArg, eventInit) {
|
|
175
|
+
// Maybe we should do some runtime eventInit validation.
|
|
176
|
+
super(typeArg, { bubbles: true, ...eventInit });
|
|
220
177
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
178
|
+
preventDefault() {
|
|
179
|
+
if (this.defaultPrevented) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
super.preventDefault();
|
|
183
|
+
const area = this.target;
|
|
184
|
+
area[_staleValue] = area[_value];
|
|
185
|
+
area[_staleSelectionRange] = area[_selectionRange];
|
|
186
|
+
const records = this.detail.mutations;
|
|
187
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
188
|
+
const record = records[i];
|
|
189
|
+
switch (record.type) {
|
|
190
|
+
case 'childList': {
|
|
191
|
+
for (let j = 0; j < record.addedNodes.length; j++) {
|
|
192
|
+
const node = record.addedNodes[j];
|
|
193
|
+
if (node.parentNode) {
|
|
194
|
+
node.parentNode.removeChild(node);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
for (let j = 0; j < record.removedNodes.length; j++) {
|
|
198
|
+
const node = record.removedNodes[j];
|
|
199
|
+
record.target.insertBefore(node, record.nextSibling);
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case 'characterData': {
|
|
204
|
+
if (record.oldValue !== null) {
|
|
205
|
+
record.target.data = record.oldValue;
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
case 'attributes': {
|
|
210
|
+
if (record.oldValue === null) {
|
|
211
|
+
record.target.removeAttribute(record.attributeName);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
record.target.setAttribute(record.attributeName, record.oldValue);
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const records1 = (area)[_observer].takeRecords();
|
|
221
|
+
validate(area, records1, PreventDefaultSource);
|
|
232
222
|
}
|
|
233
|
-
|
|
234
|
-
|
|
223
|
+
}
|
|
224
|
+
/*** NodeInfo.flags ***/
|
|
225
|
+
/** Whether the node is old. */
|
|
226
|
+
const IS_OLD = 1 << 0;
|
|
227
|
+
/** Whether the node’s info is still up-to-date. */
|
|
228
|
+
const IS_VALID = 1 << 1;
|
|
229
|
+
/** Whether the node has a styling of type display: block or similar. */
|
|
230
|
+
const IS_BLOCKLIKE = 1 << 2;
|
|
231
|
+
/** Whether the node is responsible for the newline before it. */
|
|
232
|
+
const PREPENDS_NEWLINE = 1 << 3;
|
|
233
|
+
/** Whether the node is responsible for the newline after it. */
|
|
234
|
+
const APPENDS_NEWLINE = 1 << 4;
|
|
235
|
+
/** Data associated with the child nodes of a ContentAreaElement. */
|
|
236
|
+
class NodeInfo {
|
|
237
|
+
constructor(offset) {
|
|
238
|
+
this.f = 0;
|
|
239
|
+
this.offset = offset;
|
|
240
|
+
this.length = 0;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Should be called before calling any ContentAreaElement methods.
|
|
245
|
+
*
|
|
246
|
+
* This function ensures the cache is up to date.
|
|
247
|
+
*
|
|
248
|
+
* Dispatches "contentchange" events.
|
|
249
|
+
*
|
|
250
|
+
* @returns whether a change was detected
|
|
251
|
+
*/
|
|
252
|
+
function validate(_this, records = _this[_observer].takeRecords(), source = null) {
|
|
253
|
+
if (typeof _this !== "object" || _this[_cache] == null) {
|
|
254
|
+
throw new TypeError("this is not a ContentAreaElement");
|
|
255
|
+
}
|
|
256
|
+
else if (!document.contains(_this)) {
|
|
257
|
+
throw new Error("ContentArea cannot be read before it is inserted into the DOM");
|
|
258
|
+
}
|
|
259
|
+
if (!invalidate(_this, records)) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
const oldValue = _this[_value];
|
|
263
|
+
const edit = diff(_this, oldValue, _this[_selectionRange].start);
|
|
264
|
+
_this[_value] = edit.apply(oldValue);
|
|
265
|
+
_this[_selectionRange] = getSelectionRange(_this);
|
|
266
|
+
// Don't dispatch events during composition or preventDefault operations
|
|
267
|
+
if (source !== PreventDefaultSource && !_this[_compositionBuffer]) {
|
|
268
|
+
const ev = new ContentEvent("contentchange", { detail: { edit, source, mutations: records } });
|
|
269
|
+
_this.dispatchEvent(ev);
|
|
270
|
+
_this[_staleValue] = undefined;
|
|
271
|
+
_this[_staleSelectionRange] = undefined;
|
|
235
272
|
}
|
|
236
273
|
return true;
|
|
237
274
|
}
|
|
238
|
-
function invalidate(
|
|
239
|
-
|
|
275
|
+
function invalidate(_this, records) {
|
|
276
|
+
const cache = _this[_cache];
|
|
277
|
+
if (!cache.get(_this)) {
|
|
278
|
+
// The root ContentAreaElement will not be deleted from the cache until the
|
|
279
|
+
// element is removed from the DOM, so this is the first time the
|
|
280
|
+
// ContentAreaElement is being validated.
|
|
240
281
|
return true;
|
|
241
282
|
}
|
|
242
283
|
let invalid = false;
|
|
243
284
|
for (let i = 0; i < records.length; i++) {
|
|
244
285
|
const record = records[i];
|
|
245
|
-
// We make sure all added and removed nodes
|
|
246
|
-
// in case of any
|
|
286
|
+
// We make sure all added and removed nodes and their children are deleted
|
|
287
|
+
// from the cache in case of any weirdness where nodes have been moved.
|
|
247
288
|
for (let j = 0; j < record.addedNodes.length; j++) {
|
|
248
|
-
|
|
289
|
+
const addedNode = record.addedNodes[j];
|
|
290
|
+
clear(addedNode, cache);
|
|
249
291
|
}
|
|
250
292
|
for (let j = 0; j < record.removedNodes.length; j++) {
|
|
251
293
|
clear(record.removedNodes[j], cache);
|
|
252
294
|
}
|
|
253
|
-
// TODO: invalidate data-content nodes correctly.
|
|
254
295
|
let node = record.target;
|
|
255
|
-
if (node ===
|
|
296
|
+
if (node === _this) {
|
|
256
297
|
invalid = true;
|
|
257
298
|
continue;
|
|
258
299
|
}
|
|
259
|
-
else if (!
|
|
300
|
+
else if (!_this.contains(node)) {
|
|
260
301
|
clear(node, cache);
|
|
261
302
|
continue;
|
|
262
303
|
}
|
|
263
|
-
for (; node !==
|
|
304
|
+
for (; node !== _this; node = node.parentNode) {
|
|
264
305
|
if (!cache.has(node)) {
|
|
265
306
|
break;
|
|
266
307
|
}
|
|
267
|
-
const
|
|
268
|
-
if (
|
|
269
|
-
|
|
308
|
+
const nodeInfo = cache.get(node);
|
|
309
|
+
if (nodeInfo) {
|
|
310
|
+
nodeInfo.f &= ~IS_VALID;
|
|
270
311
|
}
|
|
271
312
|
invalid = true;
|
|
272
313
|
}
|
|
273
314
|
}
|
|
274
315
|
if (invalid) {
|
|
275
|
-
const
|
|
276
|
-
|
|
316
|
+
const nodeInfo = cache.get(_this);
|
|
317
|
+
nodeInfo.f &= ~IS_VALID;
|
|
277
318
|
}
|
|
278
319
|
return invalid;
|
|
279
320
|
}
|
|
280
|
-
// This is the single most complicated function in the library!!!
|
|
281
321
|
/**
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
* future reads.
|
|
285
|
-
* @param root - The root element (usually a content-area element)
|
|
286
|
-
* @param cache - The nodeInfo cache associated with the root
|
|
287
|
-
* @param oldContent - The previous content of the root.
|
|
322
|
+
* For a given parent node and node info cache, clear the info for the node and
|
|
323
|
+
* all of its child nodes from the cache.
|
|
288
324
|
*/
|
|
289
|
-
function
|
|
290
|
-
const walker = document.createTreeWalker(
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
// read it from the DOM.
|
|
307
|
-
let oldIndex = 0;
|
|
308
|
-
// The current index into oldContent of the current node’s parent. We can get
|
|
309
|
-
// the expected start of a node if none of the nodes before it were deleted
|
|
310
|
-
// by finding the difference between oldIndex and relativeOldIndex. We can
|
|
311
|
-
// compare this difference to the cached start information to detect
|
|
312
|
-
// deletions.
|
|
313
|
-
let relativeOldIndex = 0;
|
|
314
|
-
let info = cache.get(root);
|
|
315
|
-
if (info === undefined) {
|
|
316
|
-
info = new NodeInfo(offset);
|
|
317
|
-
cache.set(root, info);
|
|
318
|
-
}
|
|
319
|
-
// A stack to save some variables as we walk up and down the tree.
|
|
325
|
+
function clear(parent, cache) {
|
|
326
|
+
const walker = document.createTreeWalker(parent, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
|
|
327
|
+
for (let node = parent; node !== null; node = walker.nextNode()) {
|
|
328
|
+
cache.delete(node);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// TODO: custom newlines?
|
|
332
|
+
const NEWLINE = "\n";
|
|
333
|
+
// THIS IS THE MOST COMPLICATED FUNCTION IN THE LIBRARY!
|
|
334
|
+
/**
|
|
335
|
+
* This function both returns an edit which represents changes to the
|
|
336
|
+
* ContentAreaElement, and populates the cache with info about nodes for future
|
|
337
|
+
* reads.
|
|
338
|
+
*/
|
|
339
|
+
function diff(_this, oldValue, oldSelectionStart) {
|
|
340
|
+
const walker = document.createTreeWalker(_this, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
|
|
341
|
+
const cache = _this[_cache];
|
|
320
342
|
const stack = [];
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
if (
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
info.flags |= PREPENDS_NEWLINE;
|
|
341
|
-
}
|
|
342
|
-
else {
|
|
343
|
-
info.flags &= ~PREPENDS_NEWLINE;
|
|
344
|
-
}
|
|
345
|
-
if ((node = walker.firstChild())) {
|
|
346
|
-
descending = true;
|
|
347
|
-
}
|
|
348
|
-
else {
|
|
349
|
-
node = walker.currentNode;
|
|
350
|
-
break;
|
|
351
|
-
}
|
|
352
|
-
stack.push({ relativeOldIndex, info });
|
|
353
|
-
relativeOldIndex = oldIndex;
|
|
354
|
-
offset = 0;
|
|
355
|
-
// getNodeInfo
|
|
356
|
-
info = cache.get(node);
|
|
357
|
-
if (info === undefined) {
|
|
358
|
-
info = new NodeInfo(offset);
|
|
359
|
-
cache.set(node, info);
|
|
343
|
+
let nodeInfo;
|
|
344
|
+
let value = "";
|
|
345
|
+
for (let node = _this, descending = true,
|
|
346
|
+
/** the current offset relative to the parent */
|
|
347
|
+
offset = 0,
|
|
348
|
+
/** the index into the old string */
|
|
349
|
+
oldIndex = 0,
|
|
350
|
+
/** the index into the old string of the parent */
|
|
351
|
+
oldIndexRelative = 0,
|
|
352
|
+
/** Whether or not the value being built currently ends with a newline */
|
|
353
|
+
hasNewline = false;; node = walker.currentNode) {
|
|
354
|
+
if (descending) {
|
|
355
|
+
// PRE-ORDER LOGIC
|
|
356
|
+
nodeInfo = cache.get(node);
|
|
357
|
+
if (nodeInfo === undefined) {
|
|
358
|
+
cache.set(node, (nodeInfo = new NodeInfo(offset)));
|
|
359
|
+
if (isBlocklikeElement(node)) {
|
|
360
|
+
nodeInfo.f |= IS_BLOCKLIKE;
|
|
361
|
+
}
|
|
360
362
|
}
|
|
361
363
|
else {
|
|
362
|
-
|
|
364
|
+
const expectedOffset = oldIndex - oldIndexRelative;
|
|
365
|
+
const deleteLength = nodeInfo.offset - expectedOffset;
|
|
366
|
+
if (deleteLength < 0) {
|
|
367
|
+
// this should never happen
|
|
368
|
+
throw new Error("cache offset error");
|
|
369
|
+
}
|
|
370
|
+
else if (deleteLength > 0) {
|
|
363
371
|
// deletion detected
|
|
364
|
-
oldIndex +=
|
|
372
|
+
oldIndex += deleteLength;
|
|
365
373
|
}
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
if (info.flags & IS_VALID) {
|
|
370
|
-
// The node has been seen before.
|
|
371
|
-
// Reading from oldContent because length hasn’t been invalidated.
|
|
372
|
-
const length = info.size;
|
|
373
|
-
if (oldIndex + info.size > oldContent.length) {
|
|
374
|
-
// This should never happen
|
|
375
|
-
throw new Error("String length mismatch");
|
|
374
|
+
nodeInfo.offset = offset;
|
|
376
375
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
376
|
+
if (offset && !hasNewline && nodeInfo.f & IS_BLOCKLIKE) {
|
|
377
|
+
// Block-like elements prepend a newline when they appear after text or
|
|
378
|
+
// inline elements.
|
|
380
379
|
hasNewline = true;
|
|
381
380
|
offset += NEWLINE.length;
|
|
382
|
-
|
|
383
|
-
|
|
381
|
+
value += NEWLINE;
|
|
382
|
+
if (nodeInfo.f & PREPENDS_NEWLINE) {
|
|
383
|
+
oldIndex += NEWLINE.length;
|
|
384
|
+
}
|
|
385
|
+
nodeInfo.f |= PREPENDS_NEWLINE;
|
|
384
386
|
}
|
|
385
387
|
else {
|
|
386
|
-
|
|
388
|
+
if (nodeInfo.f & PREPENDS_NEWLINE) {
|
|
389
|
+
// deletion detected
|
|
390
|
+
oldIndex += NEWLINE.length;
|
|
391
|
+
}
|
|
392
|
+
nodeInfo.f &= ~PREPENDS_NEWLINE;
|
|
387
393
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
+
descending = false;
|
|
395
|
+
if (nodeInfo.f & IS_VALID) {
|
|
396
|
+
// The node and its children are unchanged, so we read from the length.
|
|
397
|
+
if (nodeInfo.length) {
|
|
398
|
+
value += oldValue.slice(oldIndex, oldIndex + nodeInfo.length);
|
|
399
|
+
oldIndex += nodeInfo.length;
|
|
400
|
+
offset += nodeInfo.length;
|
|
401
|
+
hasNewline =
|
|
402
|
+
oldValue.slice(Math.max(0, oldIndex - NEWLINE.length), oldIndex) ===
|
|
403
|
+
NEWLINE;
|
|
404
|
+
}
|
|
394
405
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
hasNewline = content1.endsWith(NEWLINE);
|
|
406
|
+
else if (node.nodeType === Node.TEXT_NODE) {
|
|
407
|
+
const text = node.data;
|
|
408
|
+
if (text.length) {
|
|
409
|
+
value += text;
|
|
410
|
+
offset += text.length;
|
|
411
|
+
hasNewline = text.endsWith(NEWLINE);
|
|
412
|
+
}
|
|
413
|
+
if (nodeInfo.f & IS_OLD) {
|
|
414
|
+
oldIndex += nodeInfo.length;
|
|
405
415
|
}
|
|
406
416
|
}
|
|
407
417
|
else if (node.hasAttribute("data-content")) {
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
hasNewline =
|
|
418
|
+
const text = node.getAttribute("data-content") || "";
|
|
419
|
+
if (text.length) {
|
|
420
|
+
value += text;
|
|
421
|
+
offset += text.length;
|
|
422
|
+
hasNewline = text.endsWith(NEWLINE);
|
|
423
|
+
}
|
|
424
|
+
if (nodeInfo.f & IS_OLD) {
|
|
425
|
+
oldIndex += nodeInfo.length;
|
|
413
426
|
}
|
|
414
|
-
}
|
|
415
|
-
else if (!hasNewline && isBlocklikeElement(node)) {
|
|
416
|
-
content += NEWLINE;
|
|
417
|
-
offset += NEWLINE.length;
|
|
418
|
-
hasNewline = true;
|
|
419
|
-
appendsNewline = true;
|
|
420
427
|
}
|
|
421
428
|
else if (node.nodeName === "BR") {
|
|
422
|
-
|
|
429
|
+
value += NEWLINE;
|
|
423
430
|
offset += NEWLINE.length;
|
|
424
431
|
hasNewline = true;
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
info.flags = appendsNewline
|
|
429
|
-
? info.flags | APPENDS_NEWLINE
|
|
430
|
-
: info.flags & ~APPENDS_NEWLINE;
|
|
431
|
-
}
|
|
432
|
-
if ((node = walker.nextSibling())) {
|
|
433
|
-
descending = true;
|
|
434
|
-
// getNodeInfo
|
|
435
|
-
info = cache.get(node);
|
|
436
|
-
if (info === undefined) {
|
|
437
|
-
info = new NodeInfo(offset);
|
|
438
|
-
cache.set(node, info);
|
|
432
|
+
if (nodeInfo.f & IS_OLD) {
|
|
433
|
+
oldIndex += nodeInfo.length;
|
|
434
|
+
}
|
|
439
435
|
}
|
|
440
436
|
else {
|
|
441
|
-
|
|
442
|
-
if (
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
else if (info.offset < oldOffset) {
|
|
447
|
-
// This should never happen
|
|
448
|
-
throw new Error("Offset is before old offset");
|
|
437
|
+
descending = !!walker.firstChild();
|
|
438
|
+
if (descending) {
|
|
439
|
+
stack.push({ nodeInfo, oldIndexRelative });
|
|
440
|
+
offset = 0;
|
|
441
|
+
oldIndexRelative = oldIndex;
|
|
449
442
|
}
|
|
450
|
-
info.offset = offset;
|
|
451
443
|
}
|
|
452
444
|
}
|
|
453
445
|
else {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
446
|
+
if (!stack.length) {
|
|
447
|
+
// This should never happen.
|
|
448
|
+
throw new Error("Stack is empty");
|
|
449
|
+
}
|
|
450
|
+
// If the child node prepends a newline, add to offset to increase the
|
|
451
|
+
// length of the parent node.
|
|
452
|
+
if (nodeInfo.f & PREPENDS_NEWLINE) {
|
|
453
|
+
offset += NEWLINE.length;
|
|
454
|
+
}
|
|
455
|
+
({ nodeInfo, oldIndexRelative } = stack.pop());
|
|
456
|
+
offset = nodeInfo.offset + offset;
|
|
457
|
+
}
|
|
458
|
+
if (!descending) {
|
|
459
|
+
// POST-ORDER LOGIC
|
|
460
|
+
if (!(nodeInfo.f & IS_VALID)) {
|
|
461
|
+
// TODO: Figure out if we should always recalculate APPENDS_NEWLINE???
|
|
462
|
+
if (!hasNewline && nodeInfo.f & IS_BLOCKLIKE) {
|
|
463
|
+
value += NEWLINE;
|
|
464
|
+
offset += NEWLINE.length;
|
|
465
|
+
hasNewline = true;
|
|
466
|
+
nodeInfo.f |= APPENDS_NEWLINE;
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
nodeInfo.f &= ~APPENDS_NEWLINE;
|
|
470
|
+
}
|
|
471
|
+
nodeInfo.length = offset - nodeInfo.offset;
|
|
472
|
+
nodeInfo.f |= IS_VALID;
|
|
473
|
+
}
|
|
474
|
+
nodeInfo.f |= IS_OLD;
|
|
475
|
+
descending = !!walker.nextSibling();
|
|
476
|
+
if (!descending) {
|
|
477
|
+
if (walker.currentNode === _this) {
|
|
478
|
+
break;
|
|
459
479
|
}
|
|
460
|
-
|
|
461
|
-
offset = info.offset + offset;
|
|
462
|
-
node = walker.parentNode();
|
|
480
|
+
walker.parentNode();
|
|
463
481
|
}
|
|
464
482
|
}
|
|
483
|
+
if (oldIndex > oldValue.length) {
|
|
484
|
+
// This should never happen.
|
|
485
|
+
throw new Error("cache length error");
|
|
486
|
+
}
|
|
465
487
|
}
|
|
466
|
-
|
|
488
|
+
const selectionStart = getSelectionRange(_this).start;
|
|
489
|
+
// TODO: Doing a diff over the entirety of both oldValue and value is a
|
|
490
|
+
// performance bottleneck. Figure out how to reduce the search for changed
|
|
491
|
+
// values.
|
|
492
|
+
return Edit.diff(oldValue, value, Math.min(oldSelectionStart, selectionStart));
|
|
467
493
|
}
|
|
494
|
+
const BLOCKLIKE_DISPLAYS = new Set([
|
|
495
|
+
"block",
|
|
496
|
+
"flex",
|
|
497
|
+
"grid",
|
|
498
|
+
"flow-root",
|
|
499
|
+
"list-item",
|
|
500
|
+
"table",
|
|
501
|
+
"table-row-group",
|
|
502
|
+
"table-header-group",
|
|
503
|
+
"table-footer-group",
|
|
504
|
+
"table-row",
|
|
505
|
+
"table-caption",
|
|
506
|
+
]);
|
|
468
507
|
function isBlocklikeElement(node) {
|
|
469
|
-
return (node.nodeType === Node.ELEMENT_NODE &&
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
* child nodes from the cache.
|
|
474
|
-
*/
|
|
475
|
-
function clear(parent, cache) {
|
|
476
|
-
const walker = document.createTreeWalker(parent, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
|
|
477
|
-
for (let node = parent; node !== null; node = walker.nextNode()) {
|
|
478
|
-
cache.delete(node);
|
|
479
|
-
}
|
|
508
|
+
return (node.nodeType === Node.ELEMENT_NODE &&
|
|
509
|
+
BLOCKLIKE_DISPLAYS.has(
|
|
510
|
+
// handle two-value display syntax like `display: block flex`
|
|
511
|
+
getComputedStyle(node).display.split(" ")[0]));
|
|
480
512
|
}
|
|
513
|
+
/***********************/
|
|
514
|
+
/*** Selection Logic ***/
|
|
515
|
+
/***********************/
|
|
481
516
|
/**
|
|
482
517
|
* Finds the string index of a node and offset pair provided by a browser API
|
|
483
|
-
* like
|
|
518
|
+
* like document.getSelection() for a given root and cache.
|
|
484
519
|
*/
|
|
485
|
-
function indexAt(
|
|
486
|
-
|
|
520
|
+
function indexAt(_this, node, offset) {
|
|
521
|
+
const cache = _this[_cache];
|
|
522
|
+
if (node == null || !_this.contains(node)) {
|
|
487
523
|
return -1;
|
|
488
524
|
}
|
|
489
525
|
if (!cache.has(node)) {
|
|
490
|
-
// If the node is not found in the cache but is contained in the root,
|
|
491
|
-
//
|
|
492
|
-
// attribute.
|
|
493
|
-
// TODO: Maybe a non-zero offset should put the index at the end of
|
|
494
|
-
// the data-content node.
|
|
526
|
+
// If the node is not found in the cache but is contained in the root, then
|
|
527
|
+
// it is the child of an element with a data-content attribute.
|
|
495
528
|
offset = 0;
|
|
496
529
|
while (!cache.has(node)) {
|
|
497
530
|
node = node.parentNode;
|
|
@@ -499,8 +532,8 @@ function indexAt(root, cache, node, offset) {
|
|
|
499
532
|
}
|
|
500
533
|
let index;
|
|
501
534
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
502
|
-
const
|
|
503
|
-
index = offset +
|
|
535
|
+
const nodeInfo = cache.get(node);
|
|
536
|
+
index = offset + nodeInfo.offset;
|
|
504
537
|
node = node.parentNode;
|
|
505
538
|
}
|
|
506
539
|
else {
|
|
@@ -508,11 +541,11 @@ function indexAt(root, cache, node, offset) {
|
|
|
508
541
|
index = 0;
|
|
509
542
|
}
|
|
510
543
|
else if (offset >= node.childNodes.length) {
|
|
511
|
-
const
|
|
512
|
-
index =
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
544
|
+
const nodeInfo = cache.get(node);
|
|
545
|
+
index =
|
|
546
|
+
nodeInfo.f & APPENDS_NEWLINE
|
|
547
|
+
? nodeInfo.length - NEWLINE.length
|
|
548
|
+
: nodeInfo.length;
|
|
516
549
|
}
|
|
517
550
|
else {
|
|
518
551
|
let child = node.childNodes[offset];
|
|
@@ -524,17 +557,20 @@ function indexAt(root, cache, node, offset) {
|
|
|
524
557
|
}
|
|
525
558
|
else {
|
|
526
559
|
node = child;
|
|
527
|
-
const
|
|
560
|
+
const nodeInfo = cache.get(node);
|
|
528
561
|
// If the offset references an element which prepends a newline
|
|
529
562
|
// ("hello<div>world</div>"), we have to start from -1 because the
|
|
530
563
|
// element’s info.offset will not account for the newline.
|
|
531
|
-
index =
|
|
564
|
+
index = nodeInfo.f & PREPENDS_NEWLINE ? -1 : 0;
|
|
532
565
|
}
|
|
533
566
|
}
|
|
534
567
|
}
|
|
535
|
-
for (; node !==
|
|
536
|
-
const
|
|
537
|
-
index +=
|
|
568
|
+
for (; node !== _this; node = node.parentNode) {
|
|
569
|
+
const nodeInfo = cache.get(node);
|
|
570
|
+
index += nodeInfo.offset;
|
|
571
|
+
if (nodeInfo.f & PREPENDS_NEWLINE) {
|
|
572
|
+
index += NEWLINE.length;
|
|
573
|
+
}
|
|
538
574
|
}
|
|
539
575
|
return index;
|
|
540
576
|
}
|
|
@@ -542,47 +578,40 @@ function indexAt(root, cache, node, offset) {
|
|
|
542
578
|
* Finds the node and offset pair to use with browser APIs like
|
|
543
579
|
* selection.collapse() from a given string index.
|
|
544
580
|
*/
|
|
545
|
-
function nodeOffsetAt(
|
|
546
|
-
|
|
581
|
+
function nodeOffsetAt(_this, index) {
|
|
582
|
+
if (index < 0) {
|
|
583
|
+
return [null, 0];
|
|
584
|
+
}
|
|
585
|
+
const [node, offset] = findNodeOffset(_this, index);
|
|
547
586
|
if (node && node.nodeName === "BR") {
|
|
548
|
-
//
|
|
587
|
+
// Some browsers seem to have trouble when calling `selection.collapse()`
|
|
549
588
|
// with a BR element, so we try to avoid returning them from this function.
|
|
550
589
|
return nodeOffsetFromChild(node);
|
|
551
590
|
}
|
|
552
591
|
return [node, offset];
|
|
553
592
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
if (info == null) {
|
|
593
|
+
// TODO: Can this function be inlined?
|
|
594
|
+
function findNodeOffset(_this, index) {
|
|
595
|
+
const cache = _this[_cache];
|
|
596
|
+
const walker = document.createTreeWalker(_this, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
|
|
597
|
+
for (let node = _this; node !== null;) {
|
|
598
|
+
const nodeInfo = cache.get(node);
|
|
599
|
+
if (nodeInfo == null) {
|
|
562
600
|
return nodeOffsetFromChild(node, index > 0);
|
|
563
601
|
}
|
|
564
|
-
|
|
565
|
-
index -=
|
|
566
|
-
}
|
|
567
|
-
if (index < 0) {
|
|
568
|
-
// This branch should only run when an element prepends an newline
|
|
569
|
-
const previousSibling = walker.previousSibling();
|
|
570
|
-
if (!previousSibling) {
|
|
571
|
-
// This should never happen
|
|
572
|
-
throw new Error("Previous sibling missing");
|
|
573
|
-
}
|
|
574
|
-
return [previousSibling, getNodeLength(previousSibling)];
|
|
602
|
+
if (nodeInfo.f & PREPENDS_NEWLINE) {
|
|
603
|
+
index -= 1;
|
|
575
604
|
}
|
|
576
|
-
|
|
605
|
+
if (index === nodeInfo.length && node.nodeType === Node.TEXT_NODE) {
|
|
577
606
|
return [node, node.data.length];
|
|
578
607
|
}
|
|
579
|
-
else if (index >=
|
|
580
|
-
index -=
|
|
608
|
+
else if (index >= nodeInfo.length) {
|
|
609
|
+
index -= nodeInfo.length;
|
|
581
610
|
const nextSibling = walker.nextSibling();
|
|
582
611
|
if (nextSibling === null) {
|
|
583
612
|
// This branch seems necessary mainly when working with data-content
|
|
584
613
|
// nodes.
|
|
585
|
-
if (node ===
|
|
614
|
+
if (node === _this) {
|
|
586
615
|
return [node, getNodeLength(node)];
|
|
587
616
|
}
|
|
588
617
|
return nodeOffsetFromChild(walker.currentNode, true);
|
|
@@ -624,42 +653,41 @@ function nodeOffsetFromChild(node, after = false) {
|
|
|
624
653
|
}
|
|
625
654
|
return [parentNode, offset];
|
|
626
655
|
}
|
|
627
|
-
function getSelectionRange(
|
|
628
|
-
const selection =
|
|
656
|
+
function getSelectionRange(_this) {
|
|
657
|
+
const selection = document.getSelection();
|
|
629
658
|
if (!selection) {
|
|
630
|
-
return {
|
|
659
|
+
return { start: 0, end: 0, direction: "none" };
|
|
631
660
|
}
|
|
632
661
|
const { focusNode, focusOffset, anchorNode, anchorOffset, isCollapsed, } = selection;
|
|
633
|
-
const focus = Math.max(0, indexAt(
|
|
662
|
+
const focus = Math.max(0, indexAt(_this, focusNode, focusOffset));
|
|
634
663
|
const anchor = isCollapsed
|
|
635
664
|
? focus
|
|
636
|
-
: Math.max(0, indexAt(
|
|
665
|
+
: Math.max(0, indexAt(_this, anchorNode, anchorOffset));
|
|
637
666
|
return {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
667
|
+
start: Math.min(focus, anchor),
|
|
668
|
+
end: Math.max(focus, anchor),
|
|
669
|
+
direction: focus < anchor ? "backward" : focus > anchor ? "forward" : "none",
|
|
641
670
|
};
|
|
642
671
|
}
|
|
643
|
-
function setSelectionRange(
|
|
644
|
-
const selection =
|
|
672
|
+
function setSelectionRange(_this, { start, end, direction }) {
|
|
673
|
+
const selection = document.getSelection();
|
|
645
674
|
if (!selection) {
|
|
646
675
|
return;
|
|
647
676
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
if (
|
|
651
|
-
|
|
677
|
+
start = Math.max(0, start || 0);
|
|
678
|
+
end = Math.max(0, end || 0);
|
|
679
|
+
if (end < start) {
|
|
680
|
+
start = end;
|
|
652
681
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
const [node, offset] = nodeOffsetAt(root, cache, focusIndex);
|
|
682
|
+
// Focus is the side of the selection where the pointer is released.
|
|
683
|
+
const [focus, anchor] = direction === "backward" ? [start, end] : [end, start];
|
|
684
|
+
if (focus === anchor) {
|
|
685
|
+
const [node, offset] = nodeOffsetAt(_this, focus);
|
|
658
686
|
selection.collapse(node, offset);
|
|
659
687
|
}
|
|
660
688
|
else {
|
|
661
|
-
const [anchorNode, anchorOffset] = nodeOffsetAt(
|
|
662
|
-
const [focusNode, focusOffset] = nodeOffsetAt(
|
|
689
|
+
const [anchorNode, anchorOffset] = nodeOffsetAt(_this, anchor);
|
|
690
|
+
const [focusNode, focusOffset] = nodeOffsetAt(_this, focus);
|
|
663
691
|
if (anchorNode === null && focusNode === null) {
|
|
664
692
|
selection.collapse(null);
|
|
665
693
|
}
|
|
@@ -670,7 +698,7 @@ function setSelectionRange(root, cache, selectionStart, selectionEnd, selectionD
|
|
|
670
698
|
selection.collapse(anchorNode, anchorOffset);
|
|
671
699
|
}
|
|
672
700
|
else {
|
|
673
|
-
// This is not
|
|
701
|
+
// NOTE: This method is not implemented in IE.
|
|
674
702
|
selection.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
|
|
675
703
|
}
|
|
676
704
|
}
|