@cocreate/text 1.20.11 → 1.20.13

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
@@ -1,470 +1,470 @@
1
- /*global CustomEvent, navigator*/
2
- import observer from '@cocreate/observer';
3
- import crud from '@cocreate/crud-client';
4
- import crdt from '@cocreate/crdt';
5
- import cursors from '@cocreate/cursors';
6
- import uuid from '@cocreate/uuid';
7
- import {updateDom} from './updateDom';
8
- import {insertAdjacentElement, removeElement, setInnerText, setAttribute, removeAttribute, setClass, setStyle, replaceInnerText} from './updateText';
9
- import {getSelection, processSelection} from '@cocreate/selection';
10
- import action from '@cocreate/actions';
11
- import './saveDomText';
12
-
13
- let eventObj;
14
- let selector = `[collection][document_id][name]`;
15
- let selectors = `input${selector}, textarea${selector}, [contenteditable]${selector}:not([contenteditable='false'])`;
16
-
17
- function init() {
18
- let elements = document.querySelectorAll(selectors);
19
- initElements(elements);
20
- _crdtUpdateListener();
21
- initDocument(document);
22
- }
23
-
24
- function initElements (elements) {
25
- for(let element of elements)
26
- initElement(element);
27
- }
28
-
29
- function initElement (element) {
30
- let { collection, document_id, name, isRealtime, isCrdt, isCrud, isSave, isRead } = crud.getAttributes(element);
31
- if (!collection || !document_id || !name)
32
- return
33
- if (document_id == 'pending') {
34
- element.pendingDocument = true
35
- return
36
- }
37
- if (['_id', 'organization_id', 'db', 'database', 'collection'].includes(name))
38
- return
39
- if (isCrdt == "false" || isRealtime == "false" || element.type == 'number')
40
- return
41
- if (!crud.checkValue(collection) || !crud.checkValue(document_id)|| !crud.checkValue(name))
42
- return
43
- if (name && name.startsWith("$"))
44
- return
45
-
46
- if (element.tagName === "INPUT" && ["text", "tel", "url"].includes(element.type) || element.tagName === "TEXTAREA" || element.hasAttribute('contenteditable')) {
47
-
48
- if (!isCrdt) {
49
- if (element.tagName == 'IFRAME'){
50
- if (isCrdt != 'true')
51
- _addEventListeners(element.contentDocument.documentElement);
52
- let Document = element.contentDocument;
53
- initDocument(Document);
54
- }
55
- else if (isCrdt != 'true'){
56
- _addEventListeners(element);
57
- }
58
- }
59
- element.setAttribute('crdt', 'true');
60
- element.crdt = {init: true};
61
-
62
- // TODO: newDocument name consideration. its value is used for setting or overwriting existing value
63
- let newDocument = ''
64
- if (element.pendingDocument) {
65
-
66
- let value;
67
- if (element.hasAttribute('contenteditable'))
68
- value = element.innerHTML;
69
- else
70
- value = element.value;
71
- if (value)
72
- newDocument = value
73
-
74
- delete element.pendingDocument
75
- }
76
-
77
- crdt.getText({ collection, document_id, name, crud: isCrud, save: isSave, read: isRead, newDocument }).then(response => {
78
- if (response === undefined)
79
- return;
80
- if (!response){
81
- // if (element.pendingDocument) {
82
- // isRead = 'true'
83
- // delete element.pendingDocument
84
- // }
85
-
86
- let value;
87
- if (element.hasAttribute('contenteditable')){
88
- value = element.innerHTML;
89
- }
90
- else {
91
- value = element.value;
92
- }
93
- if (value)
94
- crdt.replaceText({ collection, document_id, name, value, crud: isCrud, save: isSave, read: isRead });
95
- }
96
- else {
97
- if (element.hasAttribute('contenteditable')){
98
- element.innerHTML = '';
99
- }
100
- else {
101
- element.value = '';
102
- }
103
- updateElement({ element, collection, document_id, name, value: response, start: 0 })
104
- }
105
- });
106
- }
107
- }
108
-
109
- function initDocument(doc) {
110
- let documents;
111
- try {
112
- documents = window.top.textDocuments;
113
- } catch(e) {
114
- console.log('cross-origin failed')
115
- }
116
-
117
- if (!documents){
118
- documents = new Map();
119
- try {
120
- window.top.textDocuments = documents;
121
- } catch(e) {
122
- console.log('cross-origin failed')
123
- }
124
- }
125
- if (!documents.has(doc)) {
126
- documents.set(doc);
127
- doc.addEventListener('selectionchange', (e) => {
128
- let element = doc.activeElement;
129
- sendPosition(element);
130
- });
131
- }
132
- }
133
-
134
- export function _addEventListeners (element) {
135
- element.addEventListener('mousedown', _mousedown);
136
- element.addEventListener('blur', _blur);
137
- element.addEventListener('cut', _cut);
138
- element.addEventListener('paste', _paste);
139
- element.addEventListener('keydown', _keydown);
140
- element.addEventListener('beforeinput', _beforeinput);
141
- element.addEventListener('input', _input);
142
- }
143
-
144
- function _mousedown (event) {
145
- let domTextEditor = event.currentTarget;
146
- if (domTextEditor.tagName === "INPUT" || domTextEditor.tagName === "TEXTAREA") return;
147
- let target = event.target;
148
- // const path = event.path || (event.composedPath && event.composedPath());
149
- // console.log(path)
150
- if (!target.id){
151
- let isEid = domTextEditor.getAttribute('eid');
152
- if (isEid != 'false' && isEid != null && isEid != undefined){
153
- let eid = target.getAttribute('eid');
154
- if (!eid){
155
- eid = uuid.generate(6);
156
- setAttribute({ domTextEditor, target, name: 'eid', value: eid });
157
- }
158
- }
159
- }
160
- let contentEditable = target.closest('[collection][document_id][name]');
161
- if (contentEditable){
162
- target = contentEditable;
163
- const { collection, document_id, name } = crud.getAttributes(target);
164
- if (collection && document_id && name && !target.hasAttribute('contenteditable'))
165
- target.setAttribute('contenteditable', 'true');
166
- }
167
- // sendPosition(element)
168
- }
169
-
170
- function _blur (event) {
171
- let element = event.currentTarget;
172
- const { collection, document_id, name } = crud.getAttributes(element);
173
- let start = null;
174
- let end = null;
175
- cursors.sendPosition({collection, document_id, name, start, end});
176
- }
177
-
178
- function _cut (event) {
179
- let element = event.currentTarget;
180
- if (element.getAttribute('crdt') == 'false')
181
- return;
182
- const { start, end, range } = getSelection(element);
183
- const selection = document.getSelection();
184
- console.log(selection.toString());
185
- if (event.clipboardData) {
186
- event.clipboardData.setData('text/plain', selection.toString());
187
- }
188
- else {
189
- navigator.clipboard.writeText(selection.toString()).then(function() {
190
- /* clipboard successfully set */
191
- }, function() {
192
- /* clipboard write failed */
193
- });
194
- }
195
- if (start != end) {
196
- updateText({element, start, end, range});
197
- }
198
- event.preventDefault();
199
- }
200
-
201
- function _paste (event) {
202
- let element = event.currentTarget;
203
- if (element.getAttribute('crdt') == 'false')
204
- return;
205
- let value = event.clipboardData.getData('text/plain').replace(/(\r\n|\r)/gm, "\n");;
206
- const { start, end, range } = getSelection(element);
207
- if (start != end) {
208
- updateText({element, start, end, range});
209
- }
210
- updateText({element, value, start, range});
211
- event.preventDefault();
212
- }
213
-
214
- function _keydown (event) {
215
- if (event.stopCCText) return;
216
- let element = event.currentTarget;
217
- if (element.getAttribute('crdt') == 'false')
218
- return;
219
- const { start, end, range } = getSelection(element);
220
- if (event.key == "Backspace" || event.key == "Tab" || event.key == "Enter") {
221
- eventObj = event;
222
- if (start != end) {
223
- updateText({element, start, end, range});
224
- }
225
-
226
- if (event.key == "Backspace" && start == end) {
227
- updateText({element, start: start - 1, end, range});
228
- }
229
- else if (event.key == 'Tab') {
230
- updateText({element, value: "\t", start, range});
231
- }
232
- else if (event.key == "Enter") {
233
- updateText({element, value: "\n", start, range});
234
- }
235
- event.preventDefault();
236
- }
237
- else if (event.ctrlKey) {
238
- if (event.keyCode == 90) {
239
- updateText({element, range, undoRedo: 'undo'});
240
- }
241
- else if (event.keyCode == 89) {
242
- updateText({element, range, undoRedo: 'redo'});
243
- }
244
- }
245
- }
246
-
247
- function _beforeinput (event) {
248
- if (event.stopCCText) return;
249
- let element = event.currentTarget;
250
- if (element.getAttribute('crdt') == 'false')
251
- return;
252
- let { start, end, range } = getSelection(element);
253
- if (event.data) {
254
- if (start != end) {
255
- updateText({element, start, end, range});
256
- }
257
- eventObj = event;
258
- updateText({element, value: event.data, start, range});
259
- event.preventDefault();
260
- }
261
- }
262
-
263
- function _input (event) {
264
- if (event.stopCCText) return;
265
- if (event.data) {
266
- eventObj = event;
267
- }
268
- }
269
-
270
- function _removeEventListeners (element) {
271
- element.removeEventListener('mousedown', _mousedown);
272
- element.removeEventListener('blur', _blur);
273
- element.removeEventListener('cut', _cut);
274
- element.removeEventListener('paste', _paste);
275
- element.removeEventListener('keydown', _keydown);
276
- element.removeEventListener('beforeinput', _beforeinput);
277
- element.removeEventListener('input', _input);
278
- }
279
-
280
- let previousPosition = {};
281
- export function sendPosition (element) {
282
- // if (!element) return;
283
- const { start, end, range } = getSelection(element);
284
- if (range) {
285
- if (range.element){
286
- element = range.element;
287
- }
288
- if (element.tagName == 'HTML' && !element.hasAttribute('collection') || !element.hasAttribute('collection')) {
289
- element = element.ownerDocument.defaultView.frameElement;
290
- }
291
- }
292
- if (!element) return;
293
- const { collection, document_id, name, isCrdt } = crud.getAttributes(element);
294
- if (isCrdt == 'false' || !collection || !document_id || !name) return;
295
- let currentPosition = { collection, document_id, name, start, end };
296
- if (JSON.stringify(currentPosition) === JSON.stringify(previousPosition))
297
- return;
298
- previousPosition = currentPosition;
299
- element.activeElement = element;
300
- window.activeElement = element;
301
- cursors.sendPosition({ collection, document_id, name, start, end });
302
- }
303
-
304
- function updateText ({element, value, start, end, range, undoRedo}) {
305
- if (range) {
306
- if (range.element)
307
- element = range.element;
308
-
309
- if (element.tagName == 'HTML' && !element.hasAttribute('collection'))
310
- element = element.ownerDocument.defaultView.frameElement;
311
- }
312
- const { collection, document_id, name, isCrud, isCrdt, isSave } = crud.getAttributes(element);
313
- if (isCrdt == "false" || !collection || !document_id || !name) return;
314
-
315
- if (undoRedo == 'undo')
316
- return crdt.undoText({ collection, document_id, name, isCrud, isCrdt, isSave })
317
- if (undoRedo == 'redo')
318
- return crdt.redoText({ collection, document_id, name, isCrud, isCrdt, isSave })
319
-
320
- let length = end - start;
321
- if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") {
322
- crdt.updateText({ collection, document_id, name, value, start, length, crud: isCrud, save: isSave });
323
- } else {
324
- let startEl = range.startContainer.parentElement;
325
- let endEl = range.endContainer.parentElement;
326
- if (startEl != endEl) {
327
- // target = range.commonAncestorContainer;
328
- // // value = target.innerHTML;
329
- // // replaceInnerText(domTextEditor, target, value)
330
- }
331
- crdt.updateText({ collection, document_id, value, name, start, length, crud: isCrud, save: isSave });
332
- }
333
- }
334
-
335
- function _crdtUpdateListener () {
336
- window.addEventListener('cocreate-crdt-update', function(event) {
337
- updateElements({...event.detail});
338
- });
339
- }
340
-
341
- function updateElements({elements, collection, document_id, name, value, start, length, string}){
342
- if (!elements){
343
- let selectors = `[collection='${collection}'][document_id='${document_id}'][name='${name}']`;
344
- elements = document.querySelectorAll(`input${selectors}, textarea${selectors}, [contenteditable]${selectors}, [editor='dom']${selectors}`);
345
- }
346
-
347
- elements.forEach((element) => {
348
- let isCrdt = element.getAttribute('crdt');
349
- if (!element.hasAttribute('contenteditable') && isCrdt == 'false') return;
350
-
351
- updateElement({element, collection, document_id, name, value, start, length, string});
352
- });
353
- }
354
-
355
- async function updateElement ({element, collection, document_id, name, value, start, length, string }) {
356
- if (element.tagName == 'IFRAME') {
357
- let eid = element.getAttribute('eid')
358
- element = element.contentDocument.documentElement;
359
- if (eid != 'false' && eid != null && eid != undefined)
360
- element.setAttribute('eid', eid)
361
- if (element.contenteditable != 'false')
362
- element.contentEditable = true;
363
- }
364
- if (value || length) {
365
- if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") {
366
- if (length) {
367
- _updateElementText(element, "", start, start + length);
368
- }
369
- if (value) {
370
- _updateElementText(element, value, start, start);
371
- }
372
- }
373
- else {
374
- let domTextEditor = element;
375
- if (string == undefined)
376
- string = await crdt.getText({collection, document_id, name});
377
- let html = string;
378
- if (length) {
379
- let end = start + length;
380
- updateDom({ domTextEditor, start, end, html });
381
- }
382
- if (value) {
383
- if (element.innerHTML != value) {
384
- updateDom({ domTextEditor, value, start, end: start, html });
385
- }
386
- }
387
- }
388
- }
389
- }
390
-
391
- function _updateElementText (element, value, start, end) {
392
- if (element.tagName === "INPUT" && ["text", "tel", "url"].includes(element.type) || element.tagName === "TEXTAREA") {
393
- let prev_start = element.selectionStart;
394
- let prev_end = element.selectionEnd;
395
- let activeElement = element.ownerDocument.activeElement;
396
- element.setRangeText(value, start, end, "end");
397
- let p = processSelection(element, value, prev_start, prev_end, start, end);
398
- if (activeElement == element)
399
- sendPosition(element);
400
- _dispatchInputEvent(element, p.value, p.start, p.end, p.prev_start, p.prev_end);
401
- }
402
- }
403
-
404
- export function _dispatchInputEvent(element, content, start, end, prev_start, prev_end) {
405
- let detail = {value: content, start, end, prev_start, prev_end, skip: true};
406
- let activeElement = element.ownerDocument.activeElement;
407
- if (activeElement == element)
408
- detail.skip = false;
409
- if (eventObj) {
410
- let event = new CustomEvent(eventObj.type, { bubbles: true });
411
- Object.defineProperty(event, 'stopCCText', { writable: false, value: true });
412
- Object.defineProperty(event, 'target', { writable: false, value: element });
413
- Object.defineProperty(event, 'detail', { writable: false, value: detail });
414
- element.dispatchEvent(event);
415
- }
416
- let inputEvent = new CustomEvent('input', { bubbles: true });
417
- Object.defineProperty(inputEvent, 'stopCCText', { writable: false, value: true });
418
- Object.defineProperty(inputEvent, 'target', { writable: false, value: element });
419
- Object.defineProperty(inputEvent, 'detail', { writable: false, value: detail });
420
- element.dispatchEvent(inputEvent);
421
- }
422
-
423
- observer.init({
424
- name: 'CoCreateTextAddedNodes',
425
- observe: ['addedNodes'],
426
- target: selectors,
427
- callback (mutation) {
428
- let isCrdt = mutation.target.getAttribute('crdt');
429
- if (isCrdt) return;
430
- initElement(mutation.target);
431
- }
432
- });
433
-
434
- observer.init({
435
- name: 'CoCreateTextAttribtes',
436
- observe: ['attributes'],
437
- attributeName: [...crud.getAttributeNames(['collection', 'document_id', 'name']), 'contenteditable'],
438
- target: selectors,
439
- callback (mutation) {
440
- let _id = mutation.target.getAttribute('document_id')
441
- if (!_id) {
442
- _removeEventListeners(mutation.target)
443
- mutation.target.removeAttribute('crdt')
444
- } else {
445
- initElement(mutation.target);
446
- }
447
- }
448
- });
449
-
450
- action.init({
451
- name: "undo",
452
- endEvent: "undo",
453
- callback: (btn, data) => {
454
- const { collection, document_id, name, isCrud, isCrdt, isSave } = crud.getAttributes(btn);
455
- crdt.undoText({ collection, document_id, name, isCrud, isCrdt, isSave })
456
- }
457
- });
458
-
459
- action.init({
460
- name: "redo",
461
- endEvent: "redo",
462
- callback: (btn, data) => {
463
- const { collection, document_id, name, isCrud, isCrdt, isSave } = crud.getAttributes(btn);
464
- crdt.redoText({ collection, document_id, name, isCrud, isCrdt, isSave })
465
- }
466
- });
467
-
468
- init();
469
-
470
- export default {initElements, initElement, updateText, updateElement, _addEventListeners, insertAdjacentElement, removeElement, setInnerText, setAttribute, removeAttribute, setClass, setStyle, replaceInnerText};
1
+ /*global CustomEvent, navigator*/
2
+ import observer from '@cocreate/observer';
3
+ import crud from '@cocreate/crud-client';
4
+ import crdt from '@cocreate/crdt';
5
+ import cursors from '@cocreate/cursors';
6
+ import uuid from '@cocreate/uuid';
7
+ import {updateDom} from './updateDom';
8
+ import {insertAdjacentElement, removeElement, setInnerText, setAttribute, removeAttribute, setClass, setStyle, replaceInnerText} from './updateText';
9
+ import {getSelection, processSelection} from '@cocreate/selection';
10
+ import action from '@cocreate/actions';
11
+ import './saveDomText';
12
+
13
+ let eventObj;
14
+ let selector = `[collection][document_id][name]`;
15
+ let selectors = `input${selector}, textarea${selector}, [contenteditable]${selector}:not([contenteditable='false'])`;
16
+
17
+ function init() {
18
+ let elements = document.querySelectorAll(selectors);
19
+ initElements(elements);
20
+ _crdtUpdateListener();
21
+ initDocument(document);
22
+ }
23
+
24
+ function initElements (elements) {
25
+ for(let element of elements)
26
+ initElement(element);
27
+ }
28
+
29
+ function initElement (element) {
30
+ let { collection, document_id, name, isRealtime, isCrdt, isCrud, isSave, isRead } = crud.getAttributes(element);
31
+ if (!collection || !document_id || !name)
32
+ return
33
+ if (document_id == 'pending') {
34
+ element.pendingDocument = true
35
+ return
36
+ }
37
+ if (['_id', 'organization_id', 'db', 'database', 'collection'].includes(name))
38
+ return
39
+ if (isCrdt == "false" || isRealtime == "false" || element.type == 'number')
40
+ return
41
+ if (!crud.checkValue(collection) || !crud.checkValue(document_id)|| !crud.checkValue(name))
42
+ return
43
+ if (name && name.startsWith("$"))
44
+ return
45
+
46
+ if (element.tagName === "INPUT" && ["text", "tel", "url"].includes(element.type) || element.tagName === "TEXTAREA" || element.hasAttribute('contenteditable')) {
47
+
48
+ if (!isCrdt) {
49
+ if (element.tagName == 'IFRAME'){
50
+ if (isCrdt != 'true')
51
+ _addEventListeners(element.contentDocument.documentElement);
52
+ let Document = element.contentDocument;
53
+ initDocument(Document);
54
+ }
55
+ else if (isCrdt != 'true'){
56
+ _addEventListeners(element);
57
+ }
58
+ }
59
+ element.setAttribute('crdt', 'true');
60
+ element.crdt = {init: true};
61
+
62
+ // TODO: newDocument name consideration. its value is used for setting or overwriting existing value
63
+ let newDocument = ''
64
+ if (element.pendingDocument) {
65
+
66
+ let value;
67
+ if (element.hasAttribute('contenteditable'))
68
+ value = element.innerHTML;
69
+ else
70
+ value = element.value;
71
+ if (value)
72
+ newDocument = value
73
+
74
+ delete element.pendingDocument
75
+ }
76
+
77
+ crdt.getText({ collection, document_id, name, crud: isCrud, save: isSave, read: isRead, newDocument }).then(response => {
78
+ if (response === undefined)
79
+ return;
80
+ if (!response){
81
+ // if (element.pendingDocument) {
82
+ // isRead = 'true'
83
+ // delete element.pendingDocument
84
+ // }
85
+
86
+ let value;
87
+ if (element.hasAttribute('contenteditable')){
88
+ value = element.innerHTML;
89
+ }
90
+ else {
91
+ value = element.value;
92
+ }
93
+ if (value)
94
+ crdt.replaceText({ collection, document_id, name, value, crud: isCrud, save: isSave, read: isRead });
95
+ }
96
+ else {
97
+ if (element.hasAttribute('contenteditable')){
98
+ element.innerHTML = '';
99
+ }
100
+ else {
101
+ element.value = '';
102
+ }
103
+ updateElement({ element, collection, document_id, name, value: response, start: 0 })
104
+ }
105
+ });
106
+ }
107
+ }
108
+
109
+ function initDocument(doc) {
110
+ let documents;
111
+ try {
112
+ documents = window.top.textDocuments;
113
+ } catch(e) {
114
+ console.log('cross-origin failed')
115
+ }
116
+
117
+ if (!documents){
118
+ documents = new Map();
119
+ try {
120
+ window.top.textDocuments = documents;
121
+ } catch(e) {
122
+ console.log('cross-origin failed')
123
+ }
124
+ }
125
+ if (!documents.has(doc)) {
126
+ documents.set(doc);
127
+ doc.addEventListener('selectionchange', (e) => {
128
+ let element = doc.activeElement;
129
+ sendPosition(element);
130
+ });
131
+ }
132
+ }
133
+
134
+ export function _addEventListeners (element) {
135
+ element.addEventListener('mousedown', _mousedown);
136
+ element.addEventListener('blur', _blur);
137
+ element.addEventListener('cut', _cut);
138
+ element.addEventListener('paste', _paste);
139
+ element.addEventListener('keydown', _keydown);
140
+ element.addEventListener('beforeinput', _beforeinput);
141
+ element.addEventListener('input', _input);
142
+ }
143
+
144
+ function _mousedown (event) {
145
+ let domTextEditor = event.currentTarget;
146
+ if (domTextEditor.tagName === "INPUT" || domTextEditor.tagName === "TEXTAREA") return;
147
+ let target = event.target;
148
+ // const path = event.path || (event.composedPath && event.composedPath());
149
+ // console.log(path)
150
+ if (!target.id){
151
+ let isEid = domTextEditor.getAttribute('eid');
152
+ if (isEid != 'false' && isEid != null && isEid != undefined){
153
+ let eid = target.getAttribute('eid');
154
+ if (!eid){
155
+ eid = uuid.generate(6);
156
+ setAttribute({ domTextEditor, target, name: 'eid', value: eid });
157
+ }
158
+ }
159
+ }
160
+ let contentEditable = target.closest('[collection][document_id][name]');
161
+ if (contentEditable){
162
+ target = contentEditable;
163
+ const { collection, document_id, name } = crud.getAttributes(target);
164
+ if (collection && document_id && name && !target.hasAttribute('contenteditable'))
165
+ target.setAttribute('contenteditable', 'true');
166
+ }
167
+ // sendPosition(element)
168
+ }
169
+
170
+ function _blur (event) {
171
+ let element = event.currentTarget;
172
+ const { collection, document_id, name } = crud.getAttributes(element);
173
+ let start = null;
174
+ let end = null;
175
+ cursors.sendPosition({collection, document_id, name, start, end});
176
+ }
177
+
178
+ function _cut (event) {
179
+ let element = event.currentTarget;
180
+ if (element.getAttribute('crdt') == 'false')
181
+ return;
182
+ const { start, end, range } = getSelection(element);
183
+ const selection = document.getSelection();
184
+ console.log(selection.toString());
185
+ if (event.clipboardData) {
186
+ event.clipboardData.setData('text/plain', selection.toString());
187
+ }
188
+ else {
189
+ navigator.clipboard.writeText(selection.toString()).then(function() {
190
+ /* clipboard successfully set */
191
+ }, function() {
192
+ /* clipboard write failed */
193
+ });
194
+ }
195
+ if (start != end) {
196
+ updateText({element, start, end, range});
197
+ }
198
+ event.preventDefault();
199
+ }
200
+
201
+ function _paste (event) {
202
+ let element = event.currentTarget;
203
+ if (element.getAttribute('crdt') == 'false')
204
+ return;
205
+ let value = event.clipboardData.getData('text/plain').replace(/(\r\n|\r)/gm, "\n");;
206
+ const { start, end, range } = getSelection(element);
207
+ if (start != end) {
208
+ updateText({element, start, end, range});
209
+ }
210
+ updateText({element, value, start, range});
211
+ event.preventDefault();
212
+ }
213
+
214
+ function _keydown (event) {
215
+ if (event.stopCCText) return;
216
+ let element = event.currentTarget;
217
+ if (element.getAttribute('crdt') == 'false')
218
+ return;
219
+ const { start, end, range } = getSelection(element);
220
+ if (event.key == "Backspace" || event.key == "Tab" || event.key == "Enter") {
221
+ eventObj = event;
222
+ if (start != end) {
223
+ updateText({element, start, end, range});
224
+ }
225
+
226
+ if (event.key == "Backspace" && start == end) {
227
+ updateText({element, start: start - 1, end, range});
228
+ }
229
+ else if (event.key == 'Tab') {
230
+ updateText({element, value: "\t", start, range});
231
+ }
232
+ else if (event.key == "Enter") {
233
+ updateText({element, value: "\n", start, range});
234
+ }
235
+ event.preventDefault();
236
+ }
237
+ else if (event.ctrlKey) {
238
+ if (event.keyCode == 90) {
239
+ updateText({element, range, undoRedo: 'undo'});
240
+ }
241
+ else if (event.keyCode == 89) {
242
+ updateText({element, range, undoRedo: 'redo'});
243
+ }
244
+ }
245
+ }
246
+
247
+ function _beforeinput (event) {
248
+ if (event.stopCCText) return;
249
+ let element = event.currentTarget;
250
+ if (element.getAttribute('crdt') == 'false')
251
+ return;
252
+ let { start, end, range } = getSelection(element);
253
+ if (event.data) {
254
+ if (start != end) {
255
+ updateText({element, start, end, range});
256
+ }
257
+ eventObj = event;
258
+ updateText({element, value: event.data, start, range});
259
+ event.preventDefault();
260
+ }
261
+ }
262
+
263
+ function _input (event) {
264
+ if (event.stopCCText) return;
265
+ if (event.data) {
266
+ eventObj = event;
267
+ }
268
+ }
269
+
270
+ function _removeEventListeners (element) {
271
+ element.removeEventListener('mousedown', _mousedown);
272
+ element.removeEventListener('blur', _blur);
273
+ element.removeEventListener('cut', _cut);
274
+ element.removeEventListener('paste', _paste);
275
+ element.removeEventListener('keydown', _keydown);
276
+ element.removeEventListener('beforeinput', _beforeinput);
277
+ element.removeEventListener('input', _input);
278
+ }
279
+
280
+ let previousPosition = {};
281
+ export function sendPosition (element) {
282
+ // if (!element) return;
283
+ const { start, end, range } = getSelection(element);
284
+ if (range) {
285
+ if (range.element){
286
+ element = range.element;
287
+ }
288
+ if (element.tagName == 'HTML' && !element.hasAttribute('collection') || !element.hasAttribute('collection')) {
289
+ element = element.ownerDocument.defaultView.frameElement;
290
+ }
291
+ }
292
+ if (!element) return;
293
+ const { collection, document_id, name, isCrdt } = crud.getAttributes(element);
294
+ if (isCrdt == 'false' || !collection || !document_id || !name) return;
295
+ let currentPosition = { collection, document_id, name, start, end };
296
+ if (JSON.stringify(currentPosition) === JSON.stringify(previousPosition))
297
+ return;
298
+ previousPosition = currentPosition;
299
+ element.activeElement = element;
300
+ window.activeElement = element;
301
+ cursors.sendPosition({ collection, document_id, name, start, end });
302
+ }
303
+
304
+ function updateText ({element, value, start, end, range, undoRedo}) {
305
+ if (range) {
306
+ if (range.element)
307
+ element = range.element;
308
+
309
+ if (element.tagName == 'HTML' && !element.hasAttribute('collection'))
310
+ element = element.ownerDocument.defaultView.frameElement;
311
+ }
312
+ const { collection, document_id, name, isCrud, isCrdt, isSave } = crud.getAttributes(element);
313
+ if (isCrdt == "false" || !collection || !document_id || !name) return;
314
+
315
+ if (undoRedo == 'undo')
316
+ return crdt.undoText({ collection, document_id, name, isCrud, isCrdt, isSave })
317
+ if (undoRedo == 'redo')
318
+ return crdt.redoText({ collection, document_id, name, isCrud, isCrdt, isSave })
319
+
320
+ let length = end - start;
321
+ if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") {
322
+ crdt.updateText({ collection, document_id, name, value, start, length, crud: isCrud, save: isSave });
323
+ } else {
324
+ let startEl = range.startContainer.parentElement;
325
+ let endEl = range.endContainer.parentElement;
326
+ if (startEl != endEl) {
327
+ // target = range.commonAncestorContainer;
328
+ // // value = target.innerHTML;
329
+ // // replaceInnerText(domTextEditor, target, value)
330
+ }
331
+ crdt.updateText({ collection, document_id, value, name, start, length, crud: isCrud, save: isSave });
332
+ }
333
+ }
334
+
335
+ function _crdtUpdateListener () {
336
+ window.addEventListener('cocreate-crdt-update', function(event) {
337
+ updateElements({...event.detail});
338
+ });
339
+ }
340
+
341
+ function updateElements({elements, collection, document_id, name, value, start, length, string}){
342
+ if (!elements){
343
+ let selectors = `[collection='${collection}'][document_id='${document_id}'][name='${name}']`;
344
+ elements = document.querySelectorAll(`input${selectors}, textarea${selectors}, [contenteditable]${selectors}, [editor='dom']${selectors}`);
345
+ }
346
+
347
+ elements.forEach((element) => {
348
+ let isCrdt = element.getAttribute('crdt');
349
+ if (!element.hasAttribute('contenteditable') && isCrdt == 'false') return;
350
+
351
+ updateElement({element, collection, document_id, name, value, start, length, string});
352
+ });
353
+ }
354
+
355
+ async function updateElement ({element, collection, document_id, name, value, start, length, string }) {
356
+ if (element.tagName == 'IFRAME') {
357
+ let eid = element.getAttribute('eid')
358
+ element = element.contentDocument.documentElement;
359
+ if (eid != 'false' && eid != null && eid != undefined)
360
+ element.setAttribute('eid', eid)
361
+ if (element.contenteditable != 'false')
362
+ element.contentEditable = true;
363
+ }
364
+ if (value || length) {
365
+ if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") {
366
+ if (length) {
367
+ _updateElementText(element, "", start, start + length);
368
+ }
369
+ if (value) {
370
+ _updateElementText(element, value, start, start);
371
+ }
372
+ }
373
+ else {
374
+ let domTextEditor = element;
375
+ if (string == undefined)
376
+ string = await crdt.getText({collection, document_id, name});
377
+ let html = string;
378
+ if (length) {
379
+ let end = start + length;
380
+ updateDom({ domTextEditor, start, end, html });
381
+ }
382
+ if (value) {
383
+ if (element.innerHTML != value) {
384
+ updateDom({ domTextEditor, value, start, end: start, html });
385
+ }
386
+ }
387
+ }
388
+ }
389
+ }
390
+
391
+ function _updateElementText (element, value, start, end) {
392
+ if (element.tagName === "INPUT" && ["text", "tel", "url"].includes(element.type) || element.tagName === "TEXTAREA") {
393
+ let prev_start = element.selectionStart;
394
+ let prev_end = element.selectionEnd;
395
+ let activeElement = element.ownerDocument.activeElement;
396
+ element.setRangeText(value, start, end, "end");
397
+ let p = processSelection(element, value, prev_start, prev_end, start, end);
398
+ if (activeElement == element)
399
+ sendPosition(element);
400
+ _dispatchInputEvent(element, p.value, p.start, p.end, p.prev_start, p.prev_end);
401
+ }
402
+ }
403
+
404
+ export function _dispatchInputEvent(element, content, start, end, prev_start, prev_end) {
405
+ let detail = {value: content, start, end, prev_start, prev_end, skip: true};
406
+ let activeElement = element.ownerDocument.activeElement;
407
+ if (activeElement == element)
408
+ detail.skip = false;
409
+ if (eventObj) {
410
+ let event = new CustomEvent(eventObj.type, { bubbles: true });
411
+ Object.defineProperty(event, 'stopCCText', { writable: false, value: true });
412
+ Object.defineProperty(event, 'target', { writable: false, value: element });
413
+ Object.defineProperty(event, 'detail', { writable: false, value: detail });
414
+ element.dispatchEvent(event);
415
+ }
416
+ let inputEvent = new CustomEvent('input', { bubbles: true });
417
+ Object.defineProperty(inputEvent, 'stopCCText', { writable: false, value: true });
418
+ Object.defineProperty(inputEvent, 'target', { writable: false, value: element });
419
+ Object.defineProperty(inputEvent, 'detail', { writable: false, value: detail });
420
+ element.dispatchEvent(inputEvent);
421
+ }
422
+
423
+ observer.init({
424
+ name: 'CoCreateTextAddedNodes',
425
+ observe: ['addedNodes'],
426
+ target: selectors,
427
+ callback (mutation) {
428
+ let isCrdt = mutation.target.getAttribute('crdt');
429
+ if (isCrdt) return;
430
+ initElement(mutation.target);
431
+ }
432
+ });
433
+
434
+ observer.init({
435
+ name: 'CoCreateTextAttribtes',
436
+ observe: ['attributes'],
437
+ attributeName: [...crud.getAttributeNames(['collection', 'document_id', 'name']), 'contenteditable'],
438
+ target: selectors,
439
+ callback (mutation) {
440
+ let _id = mutation.target.getAttribute('document_id')
441
+ if (!_id) {
442
+ _removeEventListeners(mutation.target)
443
+ mutation.target.removeAttribute('crdt')
444
+ } else {
445
+ initElement(mutation.target);
446
+ }
447
+ }
448
+ });
449
+
450
+ action.init({
451
+ name: "undo",
452
+ endEvent: "undo",
453
+ callback: (btn, data) => {
454
+ const { collection, document_id, name, isCrud, isCrdt, isSave } = crud.getAttributes(btn);
455
+ crdt.undoText({ collection, document_id, name, isCrud, isCrdt, isSave })
456
+ }
457
+ });
458
+
459
+ action.init({
460
+ name: "redo",
461
+ endEvent: "redo",
462
+ callback: (btn, data) => {
463
+ const { collection, document_id, name, isCrud, isCrdt, isSave } = crud.getAttributes(btn);
464
+ crdt.redoText({ collection, document_id, name, isCrud, isCrdt, isSave })
465
+ }
466
+ });
467
+
468
+ init();
469
+
470
+ export default {initElements, initElement, updateText, updateElement, _addEventListeners, insertAdjacentElement, removeElement, setInnerText, setAttribute, removeAttribute, setClass, setStyle, replaceInnerText};