@b9g/revise 0.1.3 → 0.1.4
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/LICENSE +7 -0
- package/README.md +147 -58
- package/package.json +59 -31
- package/{_subseq.d.ts → src/_subseq.d.ts} +18 -18
- package/src/contentarea.cjs +649 -0
- package/{contentarea.d.ts → src/contentarea.d.ts} +74 -75
- package/src/contentarea.js +624 -0
- package/src/edit.cjs +767 -0
- package/{edit.d.ts → src/edit.d.ts} +93 -99
- package/src/edit.js +741 -0
- package/src/history.cjs +100 -0
- package/{history.d.ts → src/history.d.ts} +13 -13
- package/src/history.js +76 -0
- package/src/keyer.cjs +93 -0
- package/{keyer.d.ts → src/keyer.d.ts} +8 -8
- package/src/keyer.js +69 -0
- package/src/state.cjs +142 -0
- package/src/state.d.ts +31 -0
- package/src/state.js +117 -0
- package/contentarea.cjs +0 -712
- package/contentarea.cjs.map +0 -1
- package/contentarea.js +0 -708
- package/contentarea.js.map +0 -1
- package/edit.cjs +0 -629
- package/edit.cjs.map +0 -1
- package/edit.js +0 -626
- package/edit.js.map +0 -1
- package/history.cjs +0 -81
- package/history.cjs.map +0 -1
- package/history.js +0 -78
- package/history.js.map +0 -1
- package/keyer.cjs +0 -67
- package/keyer.cjs.map +0 -1
- package/keyer.js +0 -64
- package/keyer.js.map +0 -1
package/contentarea.js
DELETED
|
@@ -1,708 +0,0 @@
|
|
|
1
|
-
/// <reference types="contentarea.d.ts" />
|
|
2
|
-
import { Edit } from './edit.js';
|
|
3
|
-
|
|
4
|
-
/// <reference lib="dom" />
|
|
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");
|
|
18
|
-
class ContentAreaElement extends HTMLElement {
|
|
19
|
-
constructor() {
|
|
20
|
-
super();
|
|
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);
|
|
28
|
-
});
|
|
29
|
-
this[_onselectionchange] = () => {
|
|
30
|
-
this[_selectionRange] = getSelectionRange(this);
|
|
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;
|
|
39
|
-
}
|
|
40
|
-
/******************************/
|
|
41
|
-
/*** Custom Element methods ***/
|
|
42
|
-
/******************************/
|
|
43
|
-
connectedCallback() {
|
|
44
|
-
this[_observer].observe(this, {
|
|
45
|
-
subtree: true,
|
|
46
|
-
childList: true,
|
|
47
|
-
characterData: true,
|
|
48
|
-
characterDataOldValue: true,
|
|
49
|
-
attributes: true,
|
|
50
|
-
attributeOldValue: true,
|
|
51
|
-
attributeFilter: [
|
|
52
|
-
"data-content",
|
|
53
|
-
// TODO: implement these attributes
|
|
54
|
-
//"data-contentbefore",
|
|
55
|
-
//"data-contentafter",
|
|
56
|
-
],
|
|
57
|
-
});
|
|
58
|
-
document.addEventListener("selectionchange", this[_onselectionchange],
|
|
59
|
-
// We use capture in an attempt to run before other event listeners.
|
|
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
|
-
});
|
|
106
|
-
}
|
|
107
|
-
disconnectedCallback() {
|
|
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.
|
|
113
|
-
if (document) {
|
|
114
|
-
document.removeEventListener("selectionchange", this[_onselectionchange], true);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
get value() {
|
|
118
|
-
validate(this);
|
|
119
|
-
return this[_staleValue] == null ? this[_value] : this[_staleValue];
|
|
120
|
-
}
|
|
121
|
-
get selectionStart() {
|
|
122
|
-
validate(this);
|
|
123
|
-
const range = this[_staleSelectionRange] || this[_selectionRange];
|
|
124
|
-
return range.start;
|
|
125
|
-
}
|
|
126
|
-
set selectionStart(start) {
|
|
127
|
-
validate(this);
|
|
128
|
-
const { end, direction } = getSelectionRange(this);
|
|
129
|
-
setSelectionRange(this, { start, end, direction });
|
|
130
|
-
}
|
|
131
|
-
get selectionEnd() {
|
|
132
|
-
validate(this);
|
|
133
|
-
const range = this[_staleSelectionRange] || this[_selectionRange];
|
|
134
|
-
return range.end;
|
|
135
|
-
}
|
|
136
|
-
set selectionEnd(end) {
|
|
137
|
-
validate(this);
|
|
138
|
-
const { start, direction } = getSelectionRange(this);
|
|
139
|
-
setSelectionRange(this, { start, end, direction });
|
|
140
|
-
}
|
|
141
|
-
get selectionDirection() {
|
|
142
|
-
validate(this);
|
|
143
|
-
const range = this[_staleSelectionRange] || this[_selectionRange];
|
|
144
|
-
return range.direction;
|
|
145
|
-
}
|
|
146
|
-
set selectionDirection(direction) {
|
|
147
|
-
validate(this);
|
|
148
|
-
const { start, end } = getSelectionRange(this);
|
|
149
|
-
setSelectionRange(this, { start, end, direction });
|
|
150
|
-
}
|
|
151
|
-
getSelectionRange() {
|
|
152
|
-
validate(this);
|
|
153
|
-
const range = this[_staleSelectionRange] || this[_selectionRange];
|
|
154
|
-
return { ...range };
|
|
155
|
-
}
|
|
156
|
-
setSelectionRange(start, end, direction = "none") {
|
|
157
|
-
validate(this);
|
|
158
|
-
setSelectionRange(this, { start, end, direction });
|
|
159
|
-
}
|
|
160
|
-
indexAt(node, offset) {
|
|
161
|
-
validate(this);
|
|
162
|
-
return indexAt(this, node, offset);
|
|
163
|
-
}
|
|
164
|
-
nodeOffsetAt(index) {
|
|
165
|
-
validate(this);
|
|
166
|
-
return nodeOffsetAt(this, index);
|
|
167
|
-
}
|
|
168
|
-
source(source) {
|
|
169
|
-
return validate(this, this[_observer].takeRecords(), source);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
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 });
|
|
177
|
-
}
|
|
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);
|
|
222
|
-
}
|
|
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;
|
|
272
|
-
}
|
|
273
|
-
return true;
|
|
274
|
-
}
|
|
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.
|
|
281
|
-
return true;
|
|
282
|
-
}
|
|
283
|
-
let invalid = false;
|
|
284
|
-
for (let i = 0; i < records.length; i++) {
|
|
285
|
-
const record = records[i];
|
|
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.
|
|
288
|
-
for (let j = 0; j < record.addedNodes.length; j++) {
|
|
289
|
-
const addedNode = record.addedNodes[j];
|
|
290
|
-
clear(addedNode, cache);
|
|
291
|
-
}
|
|
292
|
-
for (let j = 0; j < record.removedNodes.length; j++) {
|
|
293
|
-
clear(record.removedNodes[j], cache);
|
|
294
|
-
}
|
|
295
|
-
let node = record.target;
|
|
296
|
-
if (node === _this) {
|
|
297
|
-
invalid = true;
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
else if (!_this.contains(node)) {
|
|
301
|
-
clear(node, cache);
|
|
302
|
-
continue;
|
|
303
|
-
}
|
|
304
|
-
for (; node !== _this; node = node.parentNode) {
|
|
305
|
-
if (!cache.has(node)) {
|
|
306
|
-
break;
|
|
307
|
-
}
|
|
308
|
-
const nodeInfo = cache.get(node);
|
|
309
|
-
if (nodeInfo) {
|
|
310
|
-
nodeInfo.f &= ~IS_VALID;
|
|
311
|
-
}
|
|
312
|
-
invalid = true;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
if (invalid) {
|
|
316
|
-
const nodeInfo = cache.get(_this);
|
|
317
|
-
nodeInfo.f &= ~IS_VALID;
|
|
318
|
-
}
|
|
319
|
-
return invalid;
|
|
320
|
-
}
|
|
321
|
-
/**
|
|
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.
|
|
324
|
-
*/
|
|
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];
|
|
342
|
-
const stack = [];
|
|
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
|
-
}
|
|
362
|
-
}
|
|
363
|
-
else {
|
|
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) {
|
|
371
|
-
// deletion detected
|
|
372
|
-
oldIndex += deleteLength;
|
|
373
|
-
}
|
|
374
|
-
nodeInfo.offset = offset;
|
|
375
|
-
}
|
|
376
|
-
if (offset && !hasNewline && nodeInfo.f & IS_BLOCKLIKE) {
|
|
377
|
-
// Block-like elements prepend a newline when they appear after text or
|
|
378
|
-
// inline elements.
|
|
379
|
-
hasNewline = true;
|
|
380
|
-
offset += NEWLINE.length;
|
|
381
|
-
value += NEWLINE;
|
|
382
|
-
if (nodeInfo.f & PREPENDS_NEWLINE) {
|
|
383
|
-
oldIndex += NEWLINE.length;
|
|
384
|
-
}
|
|
385
|
-
nodeInfo.f |= PREPENDS_NEWLINE;
|
|
386
|
-
}
|
|
387
|
-
else {
|
|
388
|
-
if (nodeInfo.f & PREPENDS_NEWLINE) {
|
|
389
|
-
// deletion detected
|
|
390
|
-
oldIndex += NEWLINE.length;
|
|
391
|
-
}
|
|
392
|
-
nodeInfo.f &= ~PREPENDS_NEWLINE;
|
|
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
|
-
}
|
|
405
|
-
}
|
|
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;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
else if (node.hasAttribute("data-content")) {
|
|
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;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
else if (node.nodeName === "BR") {
|
|
429
|
-
value += NEWLINE;
|
|
430
|
-
offset += NEWLINE.length;
|
|
431
|
-
hasNewline = true;
|
|
432
|
-
if (nodeInfo.f & IS_OLD) {
|
|
433
|
-
oldIndex += nodeInfo.length;
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
else {
|
|
437
|
-
descending = !!walker.firstChild();
|
|
438
|
-
if (descending) {
|
|
439
|
-
stack.push({ nodeInfo, oldIndexRelative });
|
|
440
|
-
offset = 0;
|
|
441
|
-
oldIndexRelative = oldIndex;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
else {
|
|
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;
|
|
479
|
-
}
|
|
480
|
-
walker.parentNode();
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
if (oldIndex > oldValue.length) {
|
|
484
|
-
// This should never happen.
|
|
485
|
-
throw new Error("cache length error");
|
|
486
|
-
}
|
|
487
|
-
}
|
|
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));
|
|
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
|
-
]);
|
|
507
|
-
function isBlocklikeElement(node) {
|
|
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]));
|
|
512
|
-
}
|
|
513
|
-
/***********************/
|
|
514
|
-
/*** Selection Logic ***/
|
|
515
|
-
/***********************/
|
|
516
|
-
/**
|
|
517
|
-
* Finds the string index of a node and offset pair provided by a browser API
|
|
518
|
-
* like document.getSelection() for a given root and cache.
|
|
519
|
-
*/
|
|
520
|
-
function indexAt(_this, node, offset) {
|
|
521
|
-
const cache = _this[_cache];
|
|
522
|
-
if (node == null || !_this.contains(node)) {
|
|
523
|
-
return -1;
|
|
524
|
-
}
|
|
525
|
-
if (!cache.has(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.
|
|
528
|
-
offset = 0;
|
|
529
|
-
while (!cache.has(node)) {
|
|
530
|
-
node = node.parentNode;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
let index;
|
|
534
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
535
|
-
const nodeInfo = cache.get(node);
|
|
536
|
-
index = offset + nodeInfo.offset;
|
|
537
|
-
node = node.parentNode;
|
|
538
|
-
}
|
|
539
|
-
else {
|
|
540
|
-
if (offset <= 0) {
|
|
541
|
-
index = 0;
|
|
542
|
-
}
|
|
543
|
-
else if (offset >= node.childNodes.length) {
|
|
544
|
-
const nodeInfo = cache.get(node);
|
|
545
|
-
index =
|
|
546
|
-
nodeInfo.f & APPENDS_NEWLINE
|
|
547
|
-
? nodeInfo.length - NEWLINE.length
|
|
548
|
-
: nodeInfo.length;
|
|
549
|
-
}
|
|
550
|
-
else {
|
|
551
|
-
let child = node.childNodes[offset];
|
|
552
|
-
while (child !== null && !cache.has(child)) {
|
|
553
|
-
child = child.previousSibling;
|
|
554
|
-
}
|
|
555
|
-
if (child === null) {
|
|
556
|
-
index = 0;
|
|
557
|
-
}
|
|
558
|
-
else {
|
|
559
|
-
node = child;
|
|
560
|
-
const nodeInfo = cache.get(node);
|
|
561
|
-
// If the offset references an element which prepends a newline
|
|
562
|
-
// ("hello<div>world</div>"), we have to start from -1 because the
|
|
563
|
-
// element’s info.offset will not account for the newline.
|
|
564
|
-
index = nodeInfo.f & PREPENDS_NEWLINE ? -1 : 0;
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
}
|
|
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
|
-
}
|
|
574
|
-
}
|
|
575
|
-
return index;
|
|
576
|
-
}
|
|
577
|
-
/**
|
|
578
|
-
* Finds the node and offset pair to use with browser APIs like
|
|
579
|
-
* selection.collapse() from a given string index.
|
|
580
|
-
*/
|
|
581
|
-
function nodeOffsetAt(_this, index) {
|
|
582
|
-
if (index < 0) {
|
|
583
|
-
return [null, 0];
|
|
584
|
-
}
|
|
585
|
-
const [node, offset] = findNodeOffset(_this, index);
|
|
586
|
-
if (node && node.nodeName === "BR") {
|
|
587
|
-
// Some browsers seem to have trouble when calling `selection.collapse()`
|
|
588
|
-
// with a BR element, so we try to avoid returning them from this function.
|
|
589
|
-
return nodeOffsetFromChild(node);
|
|
590
|
-
}
|
|
591
|
-
return [node, offset];
|
|
592
|
-
}
|
|
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) {
|
|
600
|
-
return nodeOffsetFromChild(node, index > 0);
|
|
601
|
-
}
|
|
602
|
-
if (nodeInfo.f & PREPENDS_NEWLINE) {
|
|
603
|
-
index -= 1;
|
|
604
|
-
}
|
|
605
|
-
if (index === nodeInfo.length && node.nodeType === Node.TEXT_NODE) {
|
|
606
|
-
return [node, node.data.length];
|
|
607
|
-
}
|
|
608
|
-
else if (index >= nodeInfo.length) {
|
|
609
|
-
index -= nodeInfo.length;
|
|
610
|
-
const nextSibling = walker.nextSibling();
|
|
611
|
-
if (nextSibling === null) {
|
|
612
|
-
// This branch seems necessary mainly when working with data-content
|
|
613
|
-
// nodes.
|
|
614
|
-
if (node === _this) {
|
|
615
|
-
return [node, getNodeLength(node)];
|
|
616
|
-
}
|
|
617
|
-
return nodeOffsetFromChild(walker.currentNode, true);
|
|
618
|
-
}
|
|
619
|
-
node = nextSibling;
|
|
620
|
-
}
|
|
621
|
-
else {
|
|
622
|
-
if (node.nodeType === Node.ELEMENT_NODE &&
|
|
623
|
-
node.hasAttribute("data-content")) {
|
|
624
|
-
return nodeOffsetFromChild(node, index > 0);
|
|
625
|
-
}
|
|
626
|
-
const firstChild = walker.firstChild();
|
|
627
|
-
if (firstChild === null) {
|
|
628
|
-
const offset = node.nodeType === Node.TEXT_NODE ? index : index > 0 ? 1 : 0;
|
|
629
|
-
return [node, offset];
|
|
630
|
-
}
|
|
631
|
-
else {
|
|
632
|
-
node = firstChild;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
const node = walker.currentNode;
|
|
637
|
-
return [node, getNodeLength(node)];
|
|
638
|
-
}
|
|
639
|
-
function getNodeLength(node) {
|
|
640
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
641
|
-
return node.data.length;
|
|
642
|
-
}
|
|
643
|
-
return node.childNodes.length;
|
|
644
|
-
}
|
|
645
|
-
function nodeOffsetFromChild(node, after = false) {
|
|
646
|
-
const parentNode = node.parentNode;
|
|
647
|
-
if (parentNode === null) {
|
|
648
|
-
return [null, 0];
|
|
649
|
-
}
|
|
650
|
-
let offset = Array.from(parentNode.childNodes).indexOf(node);
|
|
651
|
-
if (after) {
|
|
652
|
-
offset++;
|
|
653
|
-
}
|
|
654
|
-
return [parentNode, offset];
|
|
655
|
-
}
|
|
656
|
-
function getSelectionRange(_this) {
|
|
657
|
-
const selection = document.getSelection();
|
|
658
|
-
if (!selection) {
|
|
659
|
-
return { start: 0, end: 0, direction: "none" };
|
|
660
|
-
}
|
|
661
|
-
const { focusNode, focusOffset, anchorNode, anchorOffset, isCollapsed, } = selection;
|
|
662
|
-
const focus = Math.max(0, indexAt(_this, focusNode, focusOffset));
|
|
663
|
-
const anchor = isCollapsed
|
|
664
|
-
? focus
|
|
665
|
-
: Math.max(0, indexAt(_this, anchorNode, anchorOffset));
|
|
666
|
-
return {
|
|
667
|
-
start: Math.min(focus, anchor),
|
|
668
|
-
end: Math.max(focus, anchor),
|
|
669
|
-
direction: focus < anchor ? "backward" : focus > anchor ? "forward" : "none",
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
function setSelectionRange(_this, { start, end, direction }) {
|
|
673
|
-
const selection = document.getSelection();
|
|
674
|
-
if (!selection) {
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
|
-
start = Math.max(0, start || 0);
|
|
678
|
-
end = Math.max(0, end || 0);
|
|
679
|
-
if (end < start) {
|
|
680
|
-
start = end;
|
|
681
|
-
}
|
|
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);
|
|
686
|
-
selection.collapse(node, offset);
|
|
687
|
-
}
|
|
688
|
-
else {
|
|
689
|
-
const [anchorNode, anchorOffset] = nodeOffsetAt(_this, anchor);
|
|
690
|
-
const [focusNode, focusOffset] = nodeOffsetAt(_this, focus);
|
|
691
|
-
if (anchorNode === null && focusNode === null) {
|
|
692
|
-
selection.collapse(null);
|
|
693
|
-
}
|
|
694
|
-
else if (anchorNode === null) {
|
|
695
|
-
selection.collapse(focusNode, focusOffset);
|
|
696
|
-
}
|
|
697
|
-
else if (focusNode === null) {
|
|
698
|
-
selection.collapse(anchorNode, anchorOffset);
|
|
699
|
-
}
|
|
700
|
-
else {
|
|
701
|
-
// NOTE: This method is not implemented in IE.
|
|
702
|
-
selection.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
export { ContentAreaElement, ContentEvent };
|
|
708
|
-
//# sourceMappingURL=contentarea.js.map
|