@b9g/revise 0.1.2 → 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/contentarea.cjs CHANGED
@@ -5,43 +5,40 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  var edit = require('./edit.cjs');
6
6
 
7
7
  /// <reference lib="dom" />
8
- // TODO: custom newlines?
9
- const NEWLINE = "\n";
10
- class ContentEvent extends CustomEvent {
11
- constructor(typeArg, eventInit) {
12
- // Maybe we should do some runtime eventInit validation.
13
- super(typeArg, { bubbles: true, ...eventInit });
14
- }
15
- }
16
- /********************************************/
8
+ /***************************************************/
17
9
  /*** ContentAreaElement private property symbols ***/
18
- /********************************************/
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");
10
+ /***************************************************/
11
+ const _cache = Symbol.for("ContentArea._cache");
12
+ const _observer = Symbol.for("ContentArea._observer");
13
+ const _onselectionchange = Symbol.for("ContentArea._onselectionchange");
14
+ const _value = Symbol.for("ContentArea._value");
15
+ const _selectionRange = Symbol.for("ContentArea._selectionRange");
16
+ const _staleValue = Symbol.for("ContentArea._staleValue");
17
+ const _staleSelectionRange = Symbol.for("ContentArea._slateSelectionRange");
18
+ const _compositionBuffer = Symbol.for("ContentArea._compositionBuffer");
19
+ const _compositionStartValue = Symbol.for("ContentArea._compositionStartValue");
20
+ const _compositionSelectionRange = Symbol.for("ContentArea._compositionSelectionRange");
24
21
  class ContentAreaElement extends HTMLElement {
25
22
  constructor() {
26
23
  super();
27
24
  this[_cache] = new Map();
28
- this[_value] = "";
29
25
  this[_observer] = new MutationObserver((records) => {
26
+ if (this[_compositionBuffer]) {
27
+ // Buffer mutations during composition but still process them to keep cache in sync
28
+ this[_compositionBuffer].push(...records);
29
+ }
30
30
  validate(this, records);
31
31
  });
32
- this[_selectionStart] = 0;
33
32
  this[_onselectionchange] = () => {
34
- // We keep track of the starting node offset pair to accurately diff
35
- // edits to text nodes.
36
- validate(this);
37
- this[_selectionStart] = getSelectionRange(this).start;
33
+ this[_selectionRange] = getSelectionRange(this);
38
34
  };
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
- });
35
+ this[_value] = "";
36
+ this[_selectionRange] = { start: 0, end: 0, direction: "none" };
37
+ this[_staleValue] = undefined;
38
+ this[_staleSelectionRange] = undefined;
39
+ this[_compositionBuffer] = undefined;
40
+ this[_compositionStartValue] = undefined;
41
+ this[_compositionSelectionRange] = undefined;
45
42
  }
46
43
  /******************************/
47
44
  /*** Custom Element methods ***/
@@ -51,7 +48,9 @@ class ContentAreaElement extends HTMLElement {
51
48
  subtree: true,
52
49
  childList: true,
53
50
  characterData: true,
51
+ characterDataOldValue: true,
54
52
  attributes: true,
53
+ attributeOldValue: true,
55
54
  attributeFilter: [
56
55
  "data-content",
57
56
  // TODO: implement these attributes
@@ -59,10 +58,54 @@ class ContentAreaElement extends HTMLElement {
59
58
  //"data-contentafter",
60
59
  ],
61
60
  });
62
- validate(this);
63
61
  document.addEventListener("selectionchange", this[_onselectionchange],
64
62
  // We use capture in an attempt to run before other event listeners.
65
63
  true);
64
+ validate(this);
65
+ this[_onselectionchange]();
66
+ // Composition event handling
67
+ let processCompositionTimeout;
68
+ this.addEventListener("compositionstart", () => {
69
+ clearTimeout(processCompositionTimeout); // Cancel pending commit
70
+ if (processCompositionTimeout == null) {
71
+ this[_compositionBuffer] = [];
72
+ this[_compositionStartValue] = this[_value];
73
+ this[_compositionSelectionRange] = { ...this[_selectionRange] };
74
+ }
75
+ processCompositionTimeout = undefined;
76
+ });
77
+ const processComposition = () => {
78
+ if (this[_compositionBuffer] &&
79
+ this[_compositionBuffer].length > 0 &&
80
+ this[_compositionStartValue] !== undefined &&
81
+ this[_compositionSelectionRange] !== undefined) {
82
+ const edit$1 = edit.Edit.diff(this[_compositionStartValue], this[_value], this[_compositionSelectionRange].start);
83
+ const ev = new ContentEvent("contentchange", {
84
+ detail: { edit: edit$1, source: null, mutations: this[_compositionBuffer] }
85
+ });
86
+ this.dispatchEvent(ev);
87
+ this[_staleValue] = undefined;
88
+ this[_staleSelectionRange] = undefined;
89
+ }
90
+ this[_compositionBuffer] = undefined;
91
+ this[_compositionStartValue] = undefined;
92
+ this[_compositionSelectionRange] = undefined;
93
+ processCompositionTimeout = undefined;
94
+ };
95
+ this.addEventListener("compositionend", () => {
96
+ clearTimeout(processCompositionTimeout);
97
+ processCompositionTimeout = setTimeout(processComposition);
98
+ });
99
+ this.addEventListener("blur", () => {
100
+ clearTimeout(processCompositionTimeout);
101
+ processComposition();
102
+ });
103
+ this.addEventListener("keydown", (e) => {
104
+ if (e.key === "Escape" && this[_compositionBuffer]) {
105
+ clearTimeout(processCompositionTimeout);
106
+ processComposition();
107
+ }
108
+ });
66
109
  }
67
110
  disconnectedCallback() {
68
111
  this[_cache].clear();
@@ -76,11 +119,12 @@ class ContentAreaElement extends HTMLElement {
76
119
  }
77
120
  get value() {
78
121
  validate(this);
79
- return this[_value];
122
+ return this[_staleValue] == null ? this[_value] : this[_staleValue];
80
123
  }
81
124
  get selectionStart() {
82
125
  validate(this);
83
- return getSelectionRange(this).start;
126
+ const range = this[_staleSelectionRange] || this[_selectionRange];
127
+ return range.start;
84
128
  }
85
129
  set selectionStart(start) {
86
130
  validate(this);
@@ -89,7 +133,8 @@ class ContentAreaElement extends HTMLElement {
89
133
  }
90
134
  get selectionEnd() {
91
135
  validate(this);
92
- return getSelectionRange(this).end;
136
+ const range = this[_staleSelectionRange] || this[_selectionRange];
137
+ return range.end;
93
138
  }
94
139
  set selectionEnd(end) {
95
140
  validate(this);
@@ -98,13 +143,19 @@ class ContentAreaElement extends HTMLElement {
98
143
  }
99
144
  get selectionDirection() {
100
145
  validate(this);
101
- return getSelectionRange(this).direction;
146
+ const range = this[_staleSelectionRange] || this[_selectionRange];
147
+ return range.direction;
102
148
  }
103
149
  set selectionDirection(direction) {
104
150
  validate(this);
105
151
  const { start, end } = getSelectionRange(this);
106
152
  setSelectionRange(this, { start, end, direction });
107
153
  }
154
+ getSelectionRange() {
155
+ validate(this);
156
+ const range = this[_staleSelectionRange] || this[_selectionRange];
157
+ return { ...range };
158
+ }
108
159
  setSelectionRange(start, end, direction = "none") {
109
160
  validate(this);
110
161
  setSelectionRange(this, { start, end, direction });
@@ -121,6 +172,58 @@ class ContentAreaElement extends HTMLElement {
121
172
  return validate(this, this[_observer].takeRecords(), source);
122
173
  }
123
174
  }
175
+ const PreventDefaultSource = Symbol.for("ContentArea.PreventDefaultSource");
176
+ class ContentEvent extends CustomEvent {
177
+ constructor(typeArg, eventInit) {
178
+ // Maybe we should do some runtime eventInit validation.
179
+ super(typeArg, { bubbles: true, ...eventInit });
180
+ }
181
+ preventDefault() {
182
+ if (this.defaultPrevented) {
183
+ return;
184
+ }
185
+ super.preventDefault();
186
+ const area = this.target;
187
+ area[_staleValue] = area[_value];
188
+ area[_staleSelectionRange] = area[_selectionRange];
189
+ const records = this.detail.mutations;
190
+ for (let i = records.length - 1; i >= 0; i--) {
191
+ const record = records[i];
192
+ switch (record.type) {
193
+ case 'childList': {
194
+ for (let j = 0; j < record.addedNodes.length; j++) {
195
+ const node = record.addedNodes[j];
196
+ if (node.parentNode) {
197
+ node.parentNode.removeChild(node);
198
+ }
199
+ }
200
+ for (let j = 0; j < record.removedNodes.length; j++) {
201
+ const node = record.removedNodes[j];
202
+ record.target.insertBefore(node, record.nextSibling);
203
+ }
204
+ break;
205
+ }
206
+ case 'characterData': {
207
+ if (record.oldValue !== null) {
208
+ record.target.data = record.oldValue;
209
+ }
210
+ break;
211
+ }
212
+ case 'attributes': {
213
+ if (record.oldValue === null) {
214
+ record.target.removeAttribute(record.attributeName);
215
+ }
216
+ else {
217
+ record.target.setAttribute(record.attributeName, record.oldValue);
218
+ }
219
+ break;
220
+ }
221
+ }
222
+ }
223
+ const records1 = (area)[_observer].takeRecords();
224
+ validate(area, records1, PreventDefaultSource);
225
+ }
226
+ }
124
227
  /*** NodeInfo.flags ***/
125
228
  /** Whether the node is old. */
126
229
  const IS_OLD = 1 << 0;
@@ -135,9 +238,9 @@ const APPENDS_NEWLINE = 1 << 4;
135
238
  /** Data associated with the child nodes of a ContentAreaElement. */
136
239
  class NodeInfo {
137
240
  constructor(offset) {
241
+ this.f = 0;
138
242
  this.offset = offset;
139
243
  this.length = 0;
140
- this.flags = 0;
141
244
  }
142
245
  }
143
246
  /**
@@ -153,14 +256,23 @@ function validate(_this, records = _this[_observer].takeRecords(), source = null
153
256
  if (typeof _this !== "object" || _this[_cache] == null) {
154
257
  throw new TypeError("this is not a ContentAreaElement");
155
258
  }
259
+ else if (!document.contains(_this)) {
260
+ throw new Error("ContentArea cannot be read before it is inserted into the DOM");
261
+ }
156
262
  if (!invalidate(_this, records)) {
157
263
  return false;
158
264
  }
159
265
  const oldValue = _this[_value];
160
- const edit = diff(_this, oldValue, _this[_selectionStart]);
266
+ const edit = diff(_this, oldValue, _this[_selectionRange].start);
161
267
  _this[_value] = edit.apply(oldValue);
162
- const ev = new ContentEvent("contentchange", { detail: { edit, source } });
163
- Promise.resolve().then(() => _this.dispatchEvent(ev));
268
+ _this[_selectionRange] = getSelectionRange(_this);
269
+ // Don't dispatch events during composition or preventDefault operations
270
+ if (source !== PreventDefaultSource && !_this[_compositionBuffer]) {
271
+ const ev = new ContentEvent("contentchange", { detail: { edit, source, mutations: records } });
272
+ _this.dispatchEvent(ev);
273
+ _this[_staleValue] = undefined;
274
+ _this[_staleSelectionRange] = undefined;
275
+ }
164
276
  return true;
165
277
  }
166
278
  function invalidate(_this, records) {
@@ -177,7 +289,8 @@ function invalidate(_this, records) {
177
289
  // We make sure all added and removed nodes and their children are deleted
178
290
  // from the cache in case of any weirdness where nodes have been moved.
179
291
  for (let j = 0; j < record.addedNodes.length; j++) {
180
- clear(record.addedNodes[j], cache);
292
+ const addedNode = record.addedNodes[j];
293
+ clear(addedNode, cache);
181
294
  }
182
295
  for (let j = 0; j < record.removedNodes.length; j++) {
183
296
  clear(record.removedNodes[j], cache);
@@ -197,14 +310,14 @@ function invalidate(_this, records) {
197
310
  }
198
311
  const nodeInfo = cache.get(node);
199
312
  if (nodeInfo) {
200
- nodeInfo.flags &= ~IS_VALID;
313
+ nodeInfo.f &= ~IS_VALID;
201
314
  }
202
315
  invalid = true;
203
316
  }
204
317
  }
205
318
  if (invalid) {
206
319
  const nodeInfo = cache.get(_this);
207
- nodeInfo.flags &= ~IS_VALID;
320
+ nodeInfo.f &= ~IS_VALID;
208
321
  }
209
322
  return invalid;
210
323
  }
@@ -218,6 +331,8 @@ function clear(parent, cache) {
218
331
  cache.delete(node);
219
332
  }
220
333
  }
334
+ // TODO: custom newlines?
335
+ const NEWLINE = "\n";
221
336
  // THIS IS THE MOST COMPLICATED FUNCTION IN THE LIBRARY!
222
337
  /**
223
338
  * This function both returns an edit which represents changes to the
@@ -245,7 +360,7 @@ function diff(_this, oldValue, oldSelectionStart) {
245
360
  if (nodeInfo === undefined) {
246
361
  cache.set(node, (nodeInfo = new NodeInfo(offset)));
247
362
  if (isBlocklikeElement(node)) {
248
- nodeInfo.flags |= IS_BLOCKLIKE;
363
+ nodeInfo.f |= IS_BLOCKLIKE;
249
364
  }
250
365
  }
251
366
  else {
@@ -261,26 +376,26 @@ function diff(_this, oldValue, oldSelectionStart) {
261
376
  }
262
377
  nodeInfo.offset = offset;
263
378
  }
264
- if (offset && !hasNewline && nodeInfo.flags & IS_BLOCKLIKE) {
379
+ if (offset && !hasNewline && nodeInfo.f & IS_BLOCKLIKE) {
265
380
  // Block-like elements prepend a newline when they appear after text or
266
381
  // inline elements.
267
382
  hasNewline = true;
268
383
  offset += NEWLINE.length;
269
384
  value += NEWLINE;
270
- if (nodeInfo.flags & PREPENDS_NEWLINE) {
385
+ if (nodeInfo.f & PREPENDS_NEWLINE) {
271
386
  oldIndex += NEWLINE.length;
272
387
  }
273
- nodeInfo.flags |= PREPENDS_NEWLINE;
388
+ nodeInfo.f |= PREPENDS_NEWLINE;
274
389
  }
275
390
  else {
276
- if (nodeInfo.flags & PREPENDS_NEWLINE) {
391
+ if (nodeInfo.f & PREPENDS_NEWLINE) {
277
392
  // deletion detected
278
393
  oldIndex += NEWLINE.length;
279
394
  }
280
- nodeInfo.flags &= ~PREPENDS_NEWLINE;
395
+ nodeInfo.f &= ~PREPENDS_NEWLINE;
281
396
  }
282
397
  descending = false;
283
- if (nodeInfo.flags & IS_VALID) {
398
+ if (nodeInfo.f & IS_VALID) {
284
399
  // The node and its children are unchanged, so we read from the length.
285
400
  if (nodeInfo.length) {
286
401
  value += oldValue.slice(oldIndex, oldIndex + nodeInfo.length);
@@ -298,7 +413,7 @@ function diff(_this, oldValue, oldSelectionStart) {
298
413
  offset += text.length;
299
414
  hasNewline = text.endsWith(NEWLINE);
300
415
  }
301
- if (nodeInfo.flags & IS_OLD) {
416
+ if (nodeInfo.f & IS_OLD) {
302
417
  oldIndex += nodeInfo.length;
303
418
  }
304
419
  }
@@ -309,7 +424,7 @@ function diff(_this, oldValue, oldSelectionStart) {
309
424
  offset += text.length;
310
425
  hasNewline = text.endsWith(NEWLINE);
311
426
  }
312
- if (nodeInfo.flags & IS_OLD) {
427
+ if (nodeInfo.f & IS_OLD) {
313
428
  oldIndex += nodeInfo.length;
314
429
  }
315
430
  }
@@ -317,7 +432,7 @@ function diff(_this, oldValue, oldSelectionStart) {
317
432
  value += NEWLINE;
318
433
  offset += NEWLINE.length;
319
434
  hasNewline = true;
320
- if (nodeInfo.flags & IS_OLD) {
435
+ if (nodeInfo.f & IS_OLD) {
321
436
  oldIndex += nodeInfo.length;
322
437
  }
323
438
  }
@@ -337,7 +452,7 @@ function diff(_this, oldValue, oldSelectionStart) {
337
452
  }
338
453
  // If the child node prepends a newline, add to offset to increase the
339
454
  // length of the parent node.
340
- if (nodeInfo.flags & PREPENDS_NEWLINE) {
455
+ if (nodeInfo.f & PREPENDS_NEWLINE) {
341
456
  offset += NEWLINE.length;
342
457
  }
343
458
  ({ nodeInfo, oldIndexRelative } = stack.pop());
@@ -345,21 +460,21 @@ function diff(_this, oldValue, oldSelectionStart) {
345
460
  }
346
461
  if (!descending) {
347
462
  // POST-ORDER LOGIC
348
- if (!(nodeInfo.flags & IS_VALID)) {
463
+ if (!(nodeInfo.f & IS_VALID)) {
349
464
  // TODO: Figure out if we should always recalculate APPENDS_NEWLINE???
350
- if (!hasNewline && nodeInfo.flags & IS_BLOCKLIKE) {
465
+ if (!hasNewline && nodeInfo.f & IS_BLOCKLIKE) {
351
466
  value += NEWLINE;
352
467
  offset += NEWLINE.length;
353
468
  hasNewline = true;
354
- nodeInfo.flags |= APPENDS_NEWLINE;
469
+ nodeInfo.f |= APPENDS_NEWLINE;
355
470
  }
356
471
  else {
357
- nodeInfo.flags &= ~APPENDS_NEWLINE;
472
+ nodeInfo.f &= ~APPENDS_NEWLINE;
358
473
  }
359
474
  nodeInfo.length = offset - nodeInfo.offset;
360
- nodeInfo.flags |= IS_VALID;
475
+ nodeInfo.f |= IS_VALID;
361
476
  }
362
- nodeInfo.flags |= IS_OLD;
477
+ nodeInfo.f |= IS_OLD;
363
478
  descending = !!walker.nextSibling();
364
479
  if (!descending) {
365
480
  if (walker.currentNode === _this) {
@@ -431,7 +546,7 @@ function indexAt(_this, node, offset) {
431
546
  else if (offset >= node.childNodes.length) {
432
547
  const nodeInfo = cache.get(node);
433
548
  index =
434
- nodeInfo.flags & APPENDS_NEWLINE
549
+ nodeInfo.f & APPENDS_NEWLINE
435
550
  ? nodeInfo.length - NEWLINE.length
436
551
  : nodeInfo.length;
437
552
  }
@@ -449,14 +564,14 @@ function indexAt(_this, node, offset) {
449
564
  // If the offset references an element which prepends a newline
450
565
  // ("hello<div>world</div>"), we have to start from -1 because the
451
566
  // element’s info.offset will not account for the newline.
452
- index = nodeInfo.flags & PREPENDS_NEWLINE ? -1 : 0;
567
+ index = nodeInfo.f & PREPENDS_NEWLINE ? -1 : 0;
453
568
  }
454
569
  }
455
570
  }
456
571
  for (; node !== _this; node = node.parentNode) {
457
572
  const nodeInfo = cache.get(node);
458
573
  index += nodeInfo.offset;
459
- if (nodeInfo.flags & PREPENDS_NEWLINE) {
574
+ if (nodeInfo.f & PREPENDS_NEWLINE) {
460
575
  index += NEWLINE.length;
461
576
  }
462
577
  }
@@ -487,7 +602,7 @@ function findNodeOffset(_this, index) {
487
602
  if (nodeInfo == null) {
488
603
  return nodeOffsetFromChild(node, index > 0);
489
604
  }
490
- if (nodeInfo.flags & PREPENDS_NEWLINE) {
605
+ if (nodeInfo.f & PREPENDS_NEWLINE) {
491
606
  index -= 1;
492
607
  }
493
608
  if (index === nodeInfo.length && node.nodeType === Node.TEXT_NODE) {
@@ -586,7 +701,7 @@ function setSelectionRange(_this, { start, end, direction }) {
586
701
  selection.collapse(anchorNode, anchorOffset);
587
702
  }
588
703
  else {
589
- // This method is not implemented in IE.
704
+ // NOTE: This method is not implemented in IE.
590
705
  selection.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
591
706
  }
592
707
  }