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