@cocreate/text 1.29.2 → 1.30.0

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/src/index.js CHANGED
@@ -2,659 +2,497 @@
2
2
  import observer from "@cocreate/observer";
3
3
  import crdt from "@cocreate/crdt";
4
4
  import cursors from "@cocreate/cursors";
5
- import uuid from "@cocreate/uuid";
6
- import { getAttributes, getAttributeNames, checkValue } from "@cocreate/utils";
5
+ import { getAttributes, getAttributeNames, checkValue, uuid } from "@cocreate/utils";
7
6
  import { updateDom } from "./updateDom";
8
7
  import {
9
- insertAdjacentElement,
10
- removeElement,
11
- setInnerText,
12
- setAttribute,
13
- removeAttribute,
14
- setClass,
15
- setStyle,
16
- replaceInnerText
8
+ insertAdjacentElement,
9
+ removeElement,
10
+ setInnerText,
11
+ setAttribute,
12
+ removeAttribute,
13
+ setClass,
14
+ setStyle,
15
+ replaceInnerText
17
16
  } from "./updateText";
18
17
  import { getSelection, processSelection } from "@cocreate/selection";
19
18
  import action from "@cocreate/actions";
20
19
 
21
- let eventObj;
22
- let selector = `[realtime][array][object][key]`;
23
- let selectors = `input${selector}, textarea${selector}, [contenteditable]${selector}:not([contenteditable='false'])`;
20
+ // Encapsulated event tracker to bridge async CRDT updates with local synthetic events
21
+ const asyncEventTracker = { current: null };
24
22
 
25
- function init() {
26
- let elements = document.querySelectorAll(selectors);
27
- initElements(elements);
28
- _crdtUpdateListener();
29
- initDocument(document);
23
+ const selector = `[realtime][array][object][key]`;
24
+ const validInputs = ["text", "search", "tel", "url", "email"];
25
+ const selectors = `input${selector}, textarea${selector}, [contenteditable]${selector}:not([contenteditable='false'])`;
26
+
27
+ export function init() {
28
+ let elements = document.querySelectorAll(selectors);
29
+ initElements(elements);
30
+ _crdtUpdateListener();
31
+ initDocument(document);
30
32
  }
31
33
 
32
- function initElements(elements) {
33
- for (let element of elements) initElement(element);
34
+ export function initElements(elements) {
35
+ for (let element of elements) initElement(element);
34
36
  }
35
37
 
36
- function initElement(element) {
37
- let { array, object, key, isRealtime, isCrdt, isCrud, isSave, isRead } =
38
- getAttributes(element);
39
- if (!array || !object || !key) return;
40
- if (object == "pending") {
41
- element.pendingObject = true;
42
- return;
43
- }
44
- if (
45
- ["_id", "organization_id", "storage", "database", "array"].includes(key)
46
- )
47
- return;
48
- if (
49
- !isRealtime ||
50
- isCrdt == "false" ||
51
- isRealtime == "false" ||
52
- element.type == "number" ||
53
- element.type == "file" ||
54
- element.getAttribute("type") === "file"
55
- )
56
- return;
57
- if (!checkValue(array) || !checkValue(object) || !checkValue(key)) return;
58
- if (key && key.startsWith("$")) return;
59
-
60
- if (
61
- (element.tagName === "INPUT" &&
62
- ["text", "search", "tel", "url", "email"].includes(element.type)) ||
63
- element.tagName === "TEXTAREA" ||
64
- element.hasAttribute("contenteditable")
65
- ) {
66
- if (!isCrdt) {
67
- if (element.tagName == "IFRAME") {
68
- if (isCrdt != "true")
69
- _addEventListeners(element.contentDocument.documentElement);
70
- let Document = element.contentDocument;
71
- initDocument(Document);
72
- } else if (isCrdt != "true") {
73
- _addEventListeners(element);
74
- }
75
- }
76
- element.setAttribute("crdt", "true");
77
- element.crdt = { init: true };
78
-
79
- // TODO: newObject name consideration. its value is used for setting or overwriting existing value
80
- let newObject = "";
81
- if (element.pendingObject) {
82
- let value;
83
- if (element.hasAttribute("contenteditable"))
84
- value = element.innerHTML;
85
- else value = element.value;
86
- if (value) newObject = value;
87
-
88
- delete element.pendingObject;
89
- }
90
-
91
- crdt.getText({
92
- array,
93
- object,
94
- key,
95
- crud: isCrud,
96
- save: isSave,
97
- read: isRead,
98
- newObject
99
- }).then((response) => {
100
- if (response === undefined) return;
101
- if (!response) {
102
- // if (element.pendingObject) {
103
- // isRead = 'true'
104
- // delete element.pendingObject
105
- // }
106
-
107
- let value;
108
- if (element.hasAttribute("contenteditable")) {
109
- value = element.innerHTML;
110
- } else {
111
- value = element.value;
112
- }
113
-
114
- if (value)
115
- crdt.replaceText({
116
- array,
117
- object,
118
- key,
119
- value,
120
- crud: isCrud,
121
- save: isSave,
122
- read: isRead
123
- });
124
- } else {
125
- if (element.hasAttribute("contenteditable")) {
126
- element.innerHTML = "";
127
- } else {
128
- element.value = "";
129
- }
130
- updateElement({
131
- element,
132
- array,
133
- object,
134
- key,
135
- value: response,
136
- start: 0
137
- });
138
- }
139
- });
140
-
141
- element.getValue = async () => {
142
- return await crdt.getText({ array, object, key });
143
- };
144
- element.setValue = (value) => {
145
- crdt.replaceText({
146
- array,
147
- object,
148
- key,
149
- value,
150
- crud: isCrud,
151
- save: isSave,
152
- read: isRead
153
- });
154
- };
155
- }
38
+ export function initElement(element) {
39
+ let { array, object, key, isRealtime, isCrdt, isCrud, isSave, isRead } = getAttributes(element);
40
+
41
+ if (!array || !object || !key) return;
42
+ if (object == "pending") {
43
+ element.pendingObject = true;
44
+ return;
45
+ }
46
+
47
+ if (["_id", "organization_id", "storage", "database", "array"].includes(key)) return;
48
+
49
+ if (
50
+ !isRealtime ||
51
+ isCrdt == "false" ||
52
+ isRealtime == "false" ||
53
+ element.type == "number" ||
54
+ element.type == "file" ||
55
+ element.getAttribute("type") === "file"
56
+ ) return;
57
+
58
+ if (!checkValue(array) || !checkValue(object) || !checkValue(key)) return;
59
+ if (key && key.startsWith("$")) return;
60
+
61
+ const isInputTypeValid = element.tagName === "INPUT" && validInputs.includes(element.type);
62
+
63
+ if (isInputTypeValid || element.tagName === "TEXTAREA" || element.hasAttribute("contenteditable")) {
64
+
65
+ if (!isCrdt) {
66
+ if (element.tagName == "IFRAME") {
67
+ if (isCrdt != "true") _addEventListeners(element.contentDocument.documentElement);
68
+ initDocument(element.contentDocument);
69
+ } else if (isCrdt != "true") {
70
+ _addEventListeners(element);
71
+ }
72
+ }
73
+
74
+ element.setAttribute("crdt", "true");
75
+ element.crdt = { init: true };
76
+
77
+ let newObject = "";
78
+ if (element.pendingObject) {
79
+ let value = element.hasAttribute("contenteditable") ? element.innerHTML : element.value;
80
+ if (value) newObject = value;
81
+ delete element.pendingObject;
82
+ }
83
+
84
+ crdt.getText({
85
+ array, object, key, crud: isCrud, save: isSave, read: isRead, newObject
86
+ }).then((response) => {
87
+ if (response === undefined) return;
88
+
89
+ if (!response) {
90
+ let value = element.hasAttribute("contenteditable") ? element.innerHTML : element.value;
91
+ if (value) {
92
+ crdt.replaceText({
93
+ array, object, key, value, crud: isCrud, save: isSave, read: isRead
94
+ });
95
+ }
96
+ } else {
97
+ if (element.hasAttribute("contenteditable")) {
98
+ element.innerHTML = "";
99
+ } else {
100
+ element.value = "";
101
+ }
102
+ updateElement({ element, array, object, key, value: response, start: 0 });
103
+ }
104
+ });
105
+
106
+ // Attach convenience methods to the DOM element
107
+ element.getValue = async () => await crdt.getText({ array, object, key });
108
+ element.setValue = (value) => {
109
+ crdt.replaceText({ array, object, key, value, crud: isCrud, save: isSave, read: isRead });
110
+ };
111
+ }
156
112
  }
157
113
 
158
114
  function initDocument(doc) {
159
- let documents;
160
- try {
161
- documents = window.top.textDocuments;
162
- } catch (e) {
163
- console.log("cross-origin failed");
164
- }
165
-
166
- if (!documents) {
167
- documents = new Map();
168
- try {
169
- window.top.textDocuments = documents;
170
- } catch (e) {
171
- console.log("cross-origin failed");
172
- }
173
- }
174
- if (!documents.has(doc)) {
175
- documents.set(doc);
176
- doc.addEventListener("selectionchange", (e) => {
177
- let element = doc.activeElement;
178
- let { isRealtime, isCrdt } = getAttributes(element);
179
-
180
- if (isRealtime && isCrdt) {
181
- doc.activeSelection = getSelection(element);
182
- sendPosition(element);
183
- }
184
- });
185
- }
115
+ let documents;
116
+ try {
117
+ // Attempt to access top-level frame to share selection registries
118
+ documents = window.top.textDocuments;
119
+ if (!documents) {
120
+ documents = new Map();
121
+ window.top.textDocuments = documents;
122
+ }
123
+ } catch (e) {
124
+ // Fallback for strict cross-origin iframe security
125
+ // console.warn("Cross-origin frame detected. Defaulting to local document map.");
126
+ documents = new Map();
127
+ }
128
+
129
+ if (!documents.has(doc)) {
130
+ documents.set(doc);
131
+ doc.addEventListener("selectionchange", (e) => {
132
+ let element = doc.activeElement;
133
+ let { isRealtime, isCrdt } = getAttributes(element);
134
+
135
+ if (isRealtime && isCrdt) {
136
+ doc.activeSelection = getSelection(element);
137
+ sendPosition(element);
138
+ }
139
+ });
140
+ }
186
141
  }
187
142
 
188
143
  export function _addEventListeners(element) {
189
- element.addEventListener("mousedown", _mousedown);
190
- element.addEventListener("blur", _blur);
191
- element.addEventListener("cut", _cut);
192
- element.addEventListener("paste", _paste);
193
- element.addEventListener("keydown", _keydown);
194
- element.addEventListener("beforeinput", _beforeinput);
195
- element.addEventListener("input", _input);
144
+ element.addEventListener("mousedown", _mousedown);
145
+ element.addEventListener("blur", _blur);
146
+ element.addEventListener("cut", _cut);
147
+ element.addEventListener("paste", _paste);
148
+ element.addEventListener("keydown", _keydown);
149
+ element.addEventListener("beforeinput", _beforeinput);
150
+ element.addEventListener("input", _input);
196
151
  }
197
152
 
198
153
  function _mousedown(event) {
199
- let domTextEditor = event.currentTarget;
200
- if (
201
- domTextEditor.tagName === "INPUT" ||
202
- domTextEditor.tagName === "TEXTAREA"
203
- )
204
- return;
205
- let target = event.target;
206
- // const path = event.path || (event.composedPath && event.composedPath());
207
- // console.log(path)
208
- if (!target.id) {
209
- let isEid = domTextEditor.getAttribute("eid");
210
- if (isEid != "false" && isEid != null && isEid != undefined) {
211
- let eid = target.getAttribute("eid");
212
- if (!eid) {
213
- eid = uuid.generate(6);
214
- setAttribute({
215
- domTextEditor,
216
- target,
217
- name: "eid",
218
- value: eid
219
- });
220
- }
221
- }
222
- }
223
- let contentEditable = target.closest("[array][object][key]");
224
- if (contentEditable) {
225
- target = contentEditable;
226
- const { array, object, key } = getAttributes(target);
227
- if (array && object && key && !target.hasAttribute("contenteditable"))
228
- target.setAttribute("contenteditable", "true");
229
- }
230
- sendPosition(domTextEditor);
154
+ let domTextEditor = event.currentTarget;
155
+ if (domTextEditor.tagName === "INPUT" || domTextEditor.tagName === "TEXTAREA") return;
156
+
157
+ let target = event.target;
158
+
159
+ // Assign a unique 'eid' if one doesn't exist for OT math resolution
160
+ if (!target.id) {
161
+ let isEid = domTextEditor.getAttribute("eid");
162
+ if (isEid != "false" && isEid != null && isEid != undefined) {
163
+ let eid = target.getAttribute("eid");
164
+ if (!eid) {
165
+ eid = uuid(6);
166
+ setAttribute({ domTextEditor, target, name: "eid", value: eid });
167
+ }
168
+ }
169
+ }
170
+
171
+ let contentEditable = target.closest("[array][object][key]");
172
+ if (contentEditable) {
173
+ target = contentEditable;
174
+ const { array, object, key } = getAttributes(target);
175
+ if (array && object && key && !target.hasAttribute("contenteditable")) {
176
+ target.setAttribute("contenteditable", "true");
177
+ }
178
+ }
179
+ sendPosition(domTextEditor);
231
180
  }
232
181
 
233
182
  function _blur(event) {
234
- let element = event.currentTarget;
235
- const { array, object, key } = getAttributes(element);
236
- let start = null;
237
- let end = null;
238
- cursors.sendPosition({ array, object, key, start, end });
183
+ let element = event.currentTarget;
184
+ const { array, object, key } = getAttributes(element);
185
+ cursors.sendPosition({ array, object, key, start: null, end: null });
239
186
  }
240
187
 
241
188
  function _cut(event) {
242
- let element = event.currentTarget;
243
- if (element.getAttribute("crdt") == "false") return;
244
- const { start, end, range } = getSelection(element);
245
- const selection = document.getSelection();
246
- console.log(selection.toString());
247
- if (event.clipboardData) {
248
- event.clipboardData.setData("text/plain", selection.toString());
249
- } else {
250
- navigator.clipboard.writeText(selection.toString()).then(
251
- function () {
252
- /* clipboard successfully set */
253
- },
254
- function () {
255
- /* clipboard write failed */
256
- }
257
- );
258
- }
259
- if (start != end) {
260
- updateText({ element, start, end, range });
261
- }
262
- event.preventDefault();
189
+ let element = event.currentTarget;
190
+ if (element.getAttribute("crdt") == "false") return;
191
+
192
+ const { start, end, range } = getSelection(element);
193
+ const selection = document.getSelection();
194
+
195
+ if (event.clipboardData) {
196
+ event.clipboardData.setData("text/plain", selection.toString());
197
+ } else {
198
+ // Note: writeText returns a promise; catch added to prevent unhandled rejections
199
+ navigator.clipboard.writeText(selection.toString()).catch(err => {
200
+ console.warn("Clipboard write failed:", err);
201
+ });
202
+ }
203
+
204
+ if (start != end) {
205
+ updateText({ element, start, end, range });
206
+ }
207
+ event.preventDefault();
263
208
  }
264
209
 
265
210
  function _paste(event) {
266
- let element = event.currentTarget;
267
- if (element.getAttribute("crdt") == "false") return;
268
- let value = event.clipboardData
269
- .getData("text/plain")
270
- .replace(/(\r\n|\r)/gm, "\n");
271
- const { start, end, range } = getSelection(element);
272
- if (start != end) {
273
- updateText({ element, start, end, range });
274
- }
275
- updateText({ element, value, start, range });
276
- event.preventDefault();
211
+ let element = event.currentTarget;
212
+ if (element.getAttribute("crdt") == "false") return;
213
+
214
+ let value = event.clipboardData.getData("text/plain").replace(/(\r\n|\r)/gm, "\n");
215
+ const { start, end, range } = getSelection(element);
216
+
217
+ if (start != end) {
218
+ updateText({ element, start, end, range });
219
+ }
220
+ updateText({ element, value, start, range });
221
+ event.preventDefault();
277
222
  }
278
223
 
279
224
  function _keydown(event) {
280
- if (event.stopCCText) return;
281
- let element = event.currentTarget;
282
- if (element.getAttribute("crdt") == "false") return;
283
- const { start, end, range } = getSelection(element);
284
- if (
285
- event.key == "Backspace" ||
286
- event.key == "Tab" ||
287
- event.key == "Enter"
288
- ) {
289
- eventObj = event;
290
- if (start != end) {
291
- updateText({ element, start, end, range });
292
- }
293
-
294
- if (event.key == "Backspace" && start == end) {
295
- updateText({ element, start: start - 1, end, range });
296
- } else if (event.key == "Tab") {
297
- updateText({ element, value: "\t", start, range });
298
- } else if (event.key == "Enter") {
299
- updateText({ element, value: "\n", start, range });
300
- }
301
- event.preventDefault();
302
- } else if (event.ctrlKey) {
303
- if (event.keyCode == 90) {
304
- updateText({ element, range, undoRedo: "undo" });
305
- } else if (event.keyCode == 89) {
306
- updateText({ element, range, undoRedo: "redo" });
307
- }
308
- } else {
309
- sendPosition(element);
310
- }
225
+ if (event.stopCCText) return;
226
+
227
+ let element = event.currentTarget;
228
+ if (element.getAttribute("crdt") == "false") return;
229
+
230
+ const { start, end, range } = getSelection(element);
231
+
232
+ // Modern Undo/Redo Check (Supports Ctrl/Cmd + Z / Cmd+Shift+Z)
233
+ if (event.ctrlKey || event.metaKey) {
234
+ let key = event.key.toLowerCase();
235
+ if (key === 'z' && !event.shiftKey) {
236
+ updateText({ element, range, undoRedo: "undo" });
237
+ event.preventDefault();
238
+ return;
239
+ } else if (key === 'y' || (key === 'z' && event.shiftKey)) {
240
+ updateText({ element, range, undoRedo: "redo" });
241
+ event.preventDefault();
242
+ return;
243
+ }
244
+ }
245
+
246
+ if (["Backspace", "Tab", "Enter"].includes(event.key)) {
247
+ asyncEventTracker.current = event;
248
+
249
+ if (start != end) {
250
+ updateText({ element, start, end, range });
251
+ }
252
+
253
+ if (event.key == "Backspace" && start == end) {
254
+ updateText({ element, start: start - 1, end, range });
255
+ } else if (event.key == "Tab") {
256
+ updateText({ element, value: "\t", start, range });
257
+ } else if (event.key == "Enter") {
258
+ updateText({ element, value: "\n", start, range });
259
+ }
260
+ event.preventDefault();
261
+ } else {
262
+ sendPosition(element);
263
+ }
311
264
  }
312
265
 
313
266
  function _beforeinput(event) {
314
- if (event.stopCCText) return;
315
- let element = event.currentTarget;
316
- if (element.getAttribute("crdt") == "false") return;
317
- let { start, end, range } = getSelection(element);
318
- if (event.data) {
319
- if (start != end) {
320
- updateText({ element, start, end, range });
321
- }
322
- eventObj = event;
323
- updateText({ element, value: event.data, start, range });
324
- event.preventDefault();
325
- }
267
+ if (event.stopCCText) return;
268
+ let element = event.currentTarget;
269
+ if (element.getAttribute("crdt") == "false") return;
270
+
271
+ let { start, end, range } = getSelection(element);
272
+
273
+ if (event.data) {
274
+ if (start != end) updateText({ element, start, end, range });
275
+ asyncEventTracker.current = event;
276
+ updateText({ element, value: event.data, start, range });
277
+ event.preventDefault();
278
+ }
326
279
  }
327
280
 
328
281
  function _input(event) {
329
- if (event.stopCCText) return;
330
- if (event.data) {
331
- eventObj = event;
332
- }
282
+ if (event.stopCCText) return;
283
+ if (event.data) {
284
+ asyncEventTracker.current = event;
285
+ }
333
286
  }
334
287
 
335
288
  function _removeEventListeners(element) {
336
- element.removeEventListener("mousedown", _mousedown);
337
- element.removeEventListener("blur", _blur);
338
- element.removeEventListener("cut", _cut);
339
- element.removeEventListener("paste", _paste);
340
- element.removeEventListener("keydown", _keydown);
341
- element.removeEventListener("beforeinput", _beforeinput);
342
- element.removeEventListener("input", _input);
289
+ element.removeEventListener("mousedown", _mousedown);
290
+ element.removeEventListener("blur", _blur);
291
+ element.removeEventListener("cut", _cut);
292
+ element.removeEventListener("paste", _paste);
293
+ element.removeEventListener("keydown", _keydown);
294
+ element.removeEventListener("beforeinput", _beforeinput);
295
+ element.removeEventListener("input", _input);
343
296
  }
344
297
 
345
298
  let previousPosition = {};
346
299
  export function sendPosition(element) {
347
- // if (!element) return;
348
- const { start, end, range } = getSelection(element);
349
- if (range) {
350
- if (range.element) {
351
- element = range.element;
352
- }
353
- if (
354
- (element.tagName == "HTML" && !element.hasAttribute("array")) ||
355
- !element.hasAttribute("array")
356
- ) {
357
- element = element.ownerDocument.defaultView.frameElement;
358
- }
359
- }
360
- if (!element) return;
361
- const { array, object, key, isCrdt } = getAttributes(element);
362
- if (isCrdt == "false" || !array || !object || !key) return;
363
- let currentPosition = { array, object, key, start, end };
364
- if (JSON.stringify(currentPosition) === JSON.stringify(previousPosition))
365
- return;
366
- previousPosition = currentPosition;
367
- // console.log('activeElement: ', element)
368
- element.activeElement = element;
369
- window.activeElement = element;
370
- cursors.sendPosition({ array, object, key, start, end });
300
+ const { start, end, range } = getSelection(element);
301
+
302
+ if (range) {
303
+ if (range.element) element = range.element;
304
+ if ((element.tagName == "HTML" && !element.hasAttribute("array")) || !element.hasAttribute("array")) {
305
+ element = element.ownerDocument.defaultView.frameElement;
306
+ }
307
+ }
308
+
309
+ if (!element) return;
310
+ const { array, object, key, isCrdt } = getAttributes(element);
311
+
312
+ if (isCrdt == "false" || !array || !object || !key) return;
313
+
314
+ let currentPosition = { array, object, key, start, end };
315
+ if (JSON.stringify(currentPosition) === JSON.stringify(previousPosition)) return;
316
+
317
+ previousPosition = currentPosition;
318
+
319
+ element.activeElement = element;
320
+ window.activeElement = element;
321
+ cursors.sendPosition({ array, object, key, start, end });
371
322
  }
372
323
 
373
324
  function updateText({ element, value, start, end, range, undoRedo }) {
374
- if (range) {
375
- if (range.element) element = range.element;
376
-
377
- if (element.tagName == "HTML" && !element.hasAttribute("array"))
378
- element = element.ownerDocument.defaultView.frameElement;
379
- }
380
-
381
- if (!element) return;
382
-
383
- const { array, object, key, isCrud, isCrdt, isSave } =
384
- getAttributes(element);
385
- if (isCrdt == "false" || !array || !object || !key) return;
386
-
387
- if (undoRedo == "undo")
388
- return crdt.undoText({ array, object, key, isCrud, isCrdt, isSave });
389
- if (undoRedo == "redo")
390
- return crdt.redoText({ array, object, key, isCrud, isCrdt, isSave });
391
-
392
- let length = end - start;
393
- if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") {
394
- crdt.updateText({
395
- array,
396
- object,
397
- key,
398
- value,
399
- start,
400
- length,
401
- crud: isCrud,
402
- save: isSave
403
- });
404
- } else {
405
- let startEl = range.startContainer.parentElement;
406
- let endEl = range.endContainer.parentElement;
407
- if (startEl != endEl) {
408
- // target = range.commonAncestorContainer;
409
- // // value = target.innerHTML;
410
- // // replaceInnerText(domTextEditor, target, value)
411
- }
412
- crdt.updateText({
413
- array,
414
- object,
415
- value,
416
- key,
417
- start,
418
- length,
419
- crud: isCrud,
420
- save: isSave
421
- });
422
- }
325
+ if (range) {
326
+ if (range.element) element = range.element;
327
+ if (element.tagName == "HTML" && !element.hasAttribute("array")) {
328
+ element = element.ownerDocument.defaultView.frameElement;
329
+ }
330
+ }
331
+
332
+ if (!element) return;
333
+
334
+ const { array, object, key, isCrud, isCrdt, isSave } = getAttributes(element);
335
+ if (isCrdt == "false" || !array || !object || !key) return;
336
+
337
+ if (undoRedo == "undo") return crdt.undoText({ array, object, key, isCrud, isCrdt, isSave });
338
+ if (undoRedo == "redo") return crdt.redoText({ array, object, key, isCrud, isCrdt, isSave });
339
+
340
+ let length = end - start;
341
+ if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") {
342
+ crdt.updateText({ array, object, key, value, start, length, crud: isCrud, save: isSave });
343
+ } else {
344
+ crdt.updateText({ array, object, value, key, start, length, crud: isCrud, save: isSave });
345
+ }
423
346
  }
424
347
 
425
348
  function _crdtUpdateListener() {
426
- window.addEventListener("cocreate-crdt-update", function (event) {
427
- updateElements({ ...event.detail });
428
- });
349
+ window.addEventListener("cocreate-crdt-update", function (event) {
350
+ updateElements({ ...event.detail });
351
+ });
429
352
  }
430
353
 
431
- function updateElements({
432
- elements,
433
- array,
434
- object,
435
- key,
436
- value,
437
- start,
438
- length,
439
- string
440
- }) {
441
- if (!elements) {
442
- let selectors = `[array='${array}'][object='${object}'][key='${key}']`;
443
- elements = document.querySelectorAll(
444
- `input${selectors}, textarea${selectors}, [contenteditable]${selectors}, [editor='dom']${selectors}`
445
- );
446
- }
447
-
448
- elements.forEach((element) => {
449
- let isCrdt = element.getAttribute("crdt");
450
- if (!element.hasAttribute("contenteditable") && isCrdt == "false")
451
- return;
452
-
453
- updateElement({
454
- element,
455
- array,
456
- object,
457
- key,
458
- value,
459
- start,
460
- length,
461
- string
462
- });
463
- });
354
+ function updateElements({ elements, array, object, key, value, start, length, string }) {
355
+ if (!elements) {
356
+ let qs = `[array='${array}'][object='${object}'][key='${key}']`;
357
+ elements = document.querySelectorAll(`input${qs}, textarea${qs}, [contenteditable]${qs}, [editor='dom']${qs}`);
358
+ }
359
+
360
+ elements.forEach((element) => {
361
+ let isCrdt = element.getAttribute("crdt");
362
+ if (!element.hasAttribute("contenteditable") && isCrdt == "false") return;
363
+
364
+ updateElement({ element, array, object, key, value, start, length, string });
365
+ });
464
366
  }
465
367
 
466
- async function updateElement({
467
- element,
468
- array,
469
- object,
470
- key,
471
- value,
472
- start,
473
- length,
474
- string
475
- }) {
476
- if (element.tagName == "IFRAME") {
477
- let eid = element.getAttribute("eid");
478
- element = element.contentDocument.documentElement;
479
- if (eid != "false" && eid != null && eid != undefined)
480
- element.setAttribute("eid", eid);
481
- if (element.contenteditable != "false") element.contentEditable = true;
482
- }
483
- if (value || length) {
484
- if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") {
485
- if (length) {
486
- _updateElementText(element, "", start, start + length);
487
- }
488
- if (value) {
489
- _updateElementText(element, value, start, start);
490
- }
491
- } else {
492
- let domTextEditor = element;
493
- if (string == undefined)
494
- string = await crdt.getText({ array, object, key });
495
- let html = string;
496
- if (length) {
497
- let end = start + length;
498
- updateDom({ domTextEditor, start, end, html });
499
- }
500
- if (value) {
501
- if (element.innerHTML != value) {
502
- updateDom({
503
- domTextEditor,
504
- value,
505
- start,
506
- end: start,
507
- html
508
- });
509
- }
510
- }
511
- }
512
- }
368
+ async function updateElement({ element, array, object, key, value, start, length, string }) {
369
+ if (element.tagName == "IFRAME") {
370
+ let eid = element.getAttribute("eid");
371
+ element = element.contentDocument.documentElement;
372
+ if (eid != "false" && eid != null && eid != undefined) element.setAttribute("eid", eid);
373
+ if (element.contenteditable != "false") element.contentEditable = true;
374
+ }
375
+
376
+ if (value || length) {
377
+ if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") {
378
+ if (length) _updateElementText(element, "", start, start + length);
379
+ if (value) _updateElementText(element, value, start, start);
380
+ } else {
381
+ let domTextEditor = element;
382
+ if (string == undefined) string = await crdt.getText({ array, object, key });
383
+ let html = string;
384
+
385
+ if (length) {
386
+ let end = start + length;
387
+ updateDom({ domTextEditor, start, end, html });
388
+ }
389
+ if (value) {
390
+ if (element.innerHTML != value) {
391
+ updateDom({ domTextEditor, value, start, end: start, html });
392
+ }
393
+ }
394
+ }
395
+ }
513
396
  }
514
397
 
515
398
  function _updateElementText(element, value, start, end) {
516
- if (
517
- (element.tagName === "INPUT" &&
518
- ["text", "tel", "url"].includes(element.type)) ||
519
- element.tagName === "TEXTAREA"
520
- ) {
521
- let prev_start = element.selectionStart;
522
- let prev_end = element.selectionEnd;
523
- let activeElement = element.ownerDocument.activeElement;
524
- element.setRangeText(value, start, end, "end");
525
- let p = processSelection(
526
- element,
527
- value,
528
- prev_start,
529
- prev_end,
530
- start,
531
- end
532
- );
533
- if (activeElement == element) sendPosition(element);
534
- _dispatchInputEvent(
535
- element,
536
- p.value,
537
- p.start,
538
- p.end,
539
- p.prev_start,
540
- p.prev_end
541
- );
542
- }
399
+ if (
400
+ (element.tagName === "INPUT" && validInputs.includes(element.type)) ||
401
+ element.tagName === "TEXTAREA"
402
+ ) {
403
+ let prev_start = element.selectionStart;
404
+ let prev_end = element.selectionEnd;
405
+ let activeElement = element.ownerDocument.activeElement;
406
+
407
+ element.setRangeText(value, start, end, "end");
408
+ let p = processSelection(element, value, prev_start, prev_end, start, end);
409
+
410
+ if (activeElement == element) sendPosition(element);
411
+
412
+ _dispatchInputEvent(element, p.value, p.start, p.end, p.prev_start, p.prev_end);
413
+ }
543
414
  }
544
415
 
545
- export function _dispatchInputEvent(
546
- element,
547
- content,
548
- start,
549
- end,
550
- prev_start,
551
- prev_end
552
- ) {
553
- let detail = {
554
- value: content,
555
- start,
556
- end,
557
- prev_start,
558
- prev_end,
559
- skip: true
560
- };
561
- let activeElement = element.ownerDocument.activeElement;
562
- if (activeElement == element) detail.skip = false;
563
- if (eventObj) {
564
- let event = new CustomEvent(eventObj.type, { bubbles: true });
565
- Object.defineProperty(event, "stopCCText", {
566
- writable: false,
567
- value: true
568
- });
569
- Object.defineProperty(event, "target", {
570
- writable: false,
571
- value: element
572
- });
573
- Object.defineProperty(event, "detail", {
574
- writable: false,
575
- value: detail
576
- });
577
- element.dispatchEvent(event);
578
- }
579
- let inputEvent = new CustomEvent("input", { bubbles: true });
580
- Object.defineProperty(inputEvent, "stopCCText", {
581
- writable: false,
582
- value: true
583
- });
584
- Object.defineProperty(inputEvent, "target", {
585
- writable: false,
586
- value: element
587
- });
588
- Object.defineProperty(inputEvent, "detail", {
589
- writable: false,
590
- value: detail
591
- });
592
- element.dispatchEvent(inputEvent);
416
+ export function _dispatchInputEvent(element, content, start, end, prev_start, prev_end) {
417
+ let detail = { value: content, start, end, prev_start, prev_end, skip: true };
418
+ let activeElement = element.ownerDocument.activeElement;
419
+
420
+ if (activeElement == element) detail.skip = false;
421
+
422
+ if (asyncEventTracker.current) {
423
+ let event = new CustomEvent(asyncEventTracker.current.type, { bubbles: true });
424
+ Object.defineProperty(event, "stopCCText", { writable: false, value: true });
425
+ Object.defineProperty(event, "target", { writable: false, value: element });
426
+ Object.defineProperty(event, "detail", { writable: false, value: detail });
427
+ element.dispatchEvent(event);
428
+ }
429
+
430
+ let inputEvent = new CustomEvent("input", { bubbles: true });
431
+ Object.defineProperty(inputEvent, "stopCCText", { writable: false, value: true });
432
+ Object.defineProperty(inputEvent, "target", { writable: false, value: element });
433
+ Object.defineProperty(inputEvent, "detail", { writable: false, value: detail });
434
+ element.dispatchEvent(inputEvent);
593
435
  }
594
436
 
595
437
  observer.init({
596
- name: "CoCreateTextAddedNodes",
597
- types: ["addedNodes"],
598
- selector: selectors,
599
- callback(mutation) {
600
- let isCrdt = mutation.target.getAttribute("crdt");
601
- if (isCrdt) return;
602
- initElement(mutation.target);
603
- }
438
+ name: "CoCreateTextAddedNodes",
439
+ types: ["addedNodes"],
440
+ selector: selectors,
441
+ callback(mutation) {
442
+ let isCrdt = mutation.target.getAttribute("crdt");
443
+ if (isCrdt) return;
444
+ initElement(mutation.target);
445
+ }
604
446
  });
605
447
 
606
448
  observer.init({
607
- name: "CoCreateTextAttribtes",
608
- types: ["attributes"],
609
- attributeFilter: ["array", "object", "key"],
610
- selector: selectors,
611
- callback(mutation) {
612
- let _id = mutation.target.getAttribute("object");
613
- if (!_id) {
614
- _removeEventListeners(mutation.target);
615
- mutation.target.removeAttribute("crdt");
616
- } else {
617
- initElement(mutation.target);
618
- }
619
- }
449
+ name: "CoCreateTextAttributes", // Fixed spelling
450
+ types: ["attributes"],
451
+ attributeFilter: ["array", "object", "key"],
452
+ selector: selectors,
453
+ callback(mutation) {
454
+ let _id = mutation.target.getAttribute("object");
455
+ if (!_id) {
456
+ _removeEventListeners(mutation.target);
457
+ mutation.target.removeAttribute("crdt");
458
+ } else {
459
+ initElement(mutation.target);
460
+ }
461
+ }
620
462
  });
621
463
 
622
464
  action.init({
623
- name: "undo",
624
- endEvent: "undo",
625
- callback: (data) => {
626
- const { array, object, key, isCrud, isCrdt, isSave } = getAttributes(
627
- data.element
628
- );
629
- crdt.undoText({ array, object, key, isCrud, isCrdt, isSave });
630
- }
465
+ name: "undo",
466
+ endEvent: "undo",
467
+ callback: (data) => {
468
+ const { array, object, key, isCrud, isCrdt, isSave } = getAttributes(data.element);
469
+ crdt.undoText({ array, object, key, isCrud, isCrdt, isSave });
470
+ }
631
471
  });
632
472
 
633
473
  action.init({
634
- name: "redo",
635
- endEvent: "redo",
636
- callback: (data) => {
637
- const { array, object, key, isCrud, isCrdt, isSave } = getAttributes(
638
- data.element
639
- );
640
- crdt.redoText({ array, object, key, isCrud, isCrdt, isSave });
641
- }
474
+ name: "redo",
475
+ endEvent: "redo",
476
+ callback: (data) => {
477
+ const { array, object, key, isCrud, isCrdt, isSave } = getAttributes(data.element);
478
+ crdt.redoText({ array, object, key, isCrud, isCrdt, isSave });
479
+ }
642
480
  });
643
481
 
644
482
  init();
645
483
 
646
484
  export default {
647
- initElements,
648
- initElement,
649
- updateText,
650
- updateElement,
651
- _addEventListeners,
652
- insertAdjacentElement,
653
- removeElement,
654
- setInnerText,
655
- setAttribute,
656
- removeAttribute,
657
- setClass,
658
- setStyle,
659
- replaceInnerText
660
- };
485
+ initElements,
486
+ initElement,
487
+ updateText,
488
+ updateElement,
489
+ _addEventListeners,
490
+ insertAdjacentElement,
491
+ removeElement,
492
+ setInnerText,
493
+ setAttribute,
494
+ removeAttribute,
495
+ setClass,
496
+ setStyle,
497
+ replaceInnerText
498
+ };