@cocreate/selection 1.0.3 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/package.json +4 -3
  3. package/src/index.js +309 -197
package/CHANGELOG.md CHANGED
@@ -1,3 +1,32 @@
1
+ ## [1.1.2](https://github.com/CoCreate-app/CoCreate-selection/compare/v1.1.1...v1.1.2) (2021-10-30)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * removed for loop in getElementPosition ([20b7ff4](https://github.com/CoCreate-app/CoCreate-selection/commit/20b7ff45dc1f321a246d4fe9f22590c5b1e17c0e))
7
+
8
+ ## [1.1.1](https://github.com/CoCreate-app/CoCreate-selection/compare/v1.1.0...v1.1.1) (2021-10-29)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * update dependencies ([84943b4](https://github.com/CoCreate-app/CoCreate-selection/commit/84943b4e31b0d267580fa17cec494e55896aa98b))
14
+
15
+ # [1.1.0](https://github.com/CoCreate-app/CoCreate-selection/compare/v1.0.4...v1.1.0) (2021-10-29)
16
+
17
+
18
+ ### Features
19
+
20
+ * get elements start and end position with out the need of element_id ([cdf1a85](https://github.com/CoCreate-app/CoCreate-selection/commit/cdf1a8517ab800c66c8fce2aa9d80e9701f9dd4c))
21
+ * removed the need for element_id, uses cssPath instead ([08c7e9e](https://github.com/CoCreate-app/CoCreate-selection/commit/08c7e9ed90c2e3bc43990ef71b830b237c320392))
22
+
23
+ ## [1.0.4](https://github.com/CoCreate-app/CoCreate-selection/compare/v1.0.3...v1.0.4) (2021-10-17)
24
+
25
+
26
+ ### Bug Fixes
27
+
28
+ * update dependendies ([816f6e2](https://github.com/CoCreate-app/CoCreate-selection/commit/816f6e2bc59dcc7c1c134d65888ae9969adc8e0f))
29
+
1
30
  ## [1.0.3](https://github.com/CoCreate-app/CoCreate-selection/compare/v1.0.2...v1.0.3) (2021-10-16)
2
31
 
3
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/selection",
3
- "version": "1.0.3",
3
+ "version": "1.1.2",
4
4
  "description": "A simple selection component in vanilla javascript. Easily configured using HTML5 data-attributes and/or JavaScript API.",
5
5
  "keywords": [
6
6
  "selection",
@@ -61,7 +61,8 @@
61
61
  "webpack-log": "^3.0.1"
62
62
  },
63
63
  "dependencies": {
64
- "@cocreate/docs": "^1.2.38",
65
- "@cocreate/hosting": "^1.2.34"
64
+ "@cocreate/docs": "^1.2.43",
65
+ "@cocreate/hosting": "^1.2.37",
66
+ "@cocreate/utils": "^1.2.0"
66
67
  }
67
68
  }
package/src/index.js CHANGED
@@ -1,6 +1,11 @@
1
1
  /*globals Node*/
2
+ import {cssPath, domParser} from '@cocreate/utils';
2
3
 
3
- export function getSelection (element) {
4
+ String.prototype.customSplice = function(index, absIndex, string) {
5
+ return this.slice(0, index) + string + this.slice(index + Math.abs(absIndex));
6
+ };
7
+
8
+ export function getSelection(element) {
4
9
  if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") {
5
10
  return {
6
11
  start: element.selectionStart,
@@ -8,23 +13,49 @@ export function getSelection (element) {
8
13
  };
9
14
  }
10
15
  else {
11
- let document = element.ownerDocument;
12
- var selection = document.getSelection();
16
+ let Document = element.ownerDocument;
17
+ let selection = Document.getSelection();
13
18
  if (!selection.rangeCount) return { start: 0, end: 0 };
14
19
 
15
- var range = selection.getRangeAt(0);
16
- var start = range.startOffset;
17
- var end = range.endOffset;
20
+ let range = selection.getRangeAt(0);
21
+ let start = range.startOffset;
22
+ let end = range.endOffset;
23
+ let previousSibling = range.startContainer.previousSibling
24
+ if(previousSibling && previousSibling.nodeType == 3) {
25
+ let length = 0;
26
+ do {
27
+ length += previousSibling.length;
28
+ previousSibling = previousSibling.previousSibling;
29
+ } while (previousSibling);
30
+ start += length
31
+ end += length
32
+ }
18
33
  if(range.startContainer != range.endContainer) {
19
34
  // toDo: replace common ancestor value
20
35
  }
21
36
  let domTextEditor = element;
22
- let nodePos = getDomPosition({ domTextEditor, target: range.startContainer.parentElement, start, end });
23
- if (nodePos){
24
- start = nodePos.start;
25
- end = nodePos.end;
26
- }
27
- return { start, end, range };
37
+ if (!domTextEditor.htmlString){
38
+ domTextEditor = element.closest('[contenteditable]');
39
+ }
40
+ let elementStart = start, elementEnd = end;
41
+ if (domTextEditor){
42
+ let nodePos = getStringPosition({ string: domTextEditor.htmlString, target: range.startContainer.parentElement, position: 'afterbegin'});
43
+ if (nodePos){
44
+ elementStart = nodePos.start
45
+ elementEnd = nodePos.end
46
+ start = start + nodePos.start;
47
+ end = end + nodePos.end;
48
+ }
49
+ }
50
+ let rangeObj = {
51
+ startOffset: range.startOffset,
52
+ endOffset: range.endOffset,
53
+ startContainer: range.startContainer.parentElement,
54
+ endContainer: range.endContainer.parentElement,
55
+ elementStart,
56
+ elementEnd
57
+ };
58
+ return { start, end, range: rangeObj};
28
59
  }
29
60
 
30
61
  }
@@ -55,21 +86,34 @@ export function setSelection(element, start, end, range) {
55
86
  element.selectionEnd = end;
56
87
  }
57
88
  else {
58
- // if (document.activeElement !== element) return;
59
- if (range.commonAncestorContainer) {
60
- let prevElement = range.commonAncestorContainer;
61
- if (prevElement.nodeName == '#text')
62
- prevElement = range.commonAncestorContainer.parentElement;
63
- if (prevElement !== element) return;
64
- }
65
- let document = element.ownerDocument;
66
- var selection = document.getSelection();
67
- var range = contenteditable._cloneRangeByPosition(element, start, end);
89
+ if (!range) return;
90
+ let Document = element.ownerDocument;
91
+ let startOffset = start - range.elementStart;
92
+ let endOffset = end - range.elementEnd;
93
+ let startContainer = getContainer(range.startContainer, startOffset);
94
+ let endContainer = getContainer(range.endContainer, endOffset);
95
+
96
+ let selection = Document.getSelection();
68
97
  selection.removeAllRanges();
69
- selection.addRange(range);
70
- }
98
+ const nrange = Document.createRange();
99
+ nrange.setStart(startContainer, startOffset);
100
+ nrange.setEnd(endContainer, endOffset);
101
+ selection.addRange(nrange);
102
+ }
71
103
  }
72
104
 
105
+ function getContainer(element, offset){
106
+ let nodeLengths = 0;
107
+ for (let node of element.childNodes){
108
+ if (node.nodeType == 3) {
109
+ let length = node.length + nodeLengths;
110
+ if (length >= offset)
111
+ return node;
112
+ else
113
+ nodeLengths += length;
114
+ }
115
+ }
116
+ }
73
117
 
74
118
  export function hasSelection(el) {
75
119
  let { start, end } = getSelection(el);
@@ -78,189 +122,257 @@ export function hasSelection(el) {
78
122
  }
79
123
  }
80
124
 
81
- const contenteditable = {
82
- _cloneRangeByPosition: function(element, start, end, range) {
83
- if (!range) {
84
- range = document.createRange();
85
- range.selectNode(element);
86
- range.setStart(element, 0);
87
- this.start = start;
88
- this.end = end;
89
- }
90
-
91
- if (element && (this.start > 0 || this.end > 0)) {
92
- if (element.nodeType === Node.TEXT_NODE) {
93
-
94
- if (element.textContent.length < this.start) this.start -= element.textContent.length;
95
- else if (this.start > 0) {
96
- range.setStart(element, this.start);
97
- this.start = 0;
98
- }
99
-
100
- if (element.textContent.length < this.end) this.end -= element.textContent.length;
101
- else if (this.end > 0) {
102
- range.setEnd(element, this.end);
103
- this.end = 0;
104
- }
105
- }
106
- else {
107
- for (var lp = 0; lp < element.childNodes.length; lp++) {
108
- range = this._cloneRangeByPosition(element.childNodes[lp], this.start, this.end, range);
109
- if (this.start === 0 && this.end === 0) break;
110
- }
111
- }
112
- }
113
-
114
- return range;
115
- },
116
-
117
- };
118
-
119
-
120
-
121
- let space = "\u{0020}|\u{0009}";
122
- let allAttributeName = `[a-z0-9-_]+?`;
123
-
124
- let sps = `(${space})*?`;
125
- let spa = `(${space})+?`;
126
- let tgs = `(?:<(?<tagName>[a-z0-9]+?))`;
127
- let getEndTag = tagName => `(?:<(?<isClosing>${sps}\/${sps})?${tagName}${sps})`;
128
-
129
- const idSearch = 'element_id=';
130
- const getRegAttribute = (attributeName) =>
131
- `(${spa}(?:(?:${attributeName})((?:="[^"]*?")|${space}|>)))`;
132
-
133
- let at = getRegAttribute(allAttributeName);
134
-
135
- let the = `${sps}(?<tagSlash>\/)?${sps}>`;
136
-
137
- let target;
138
- let tagName;
139
- let tagStPos;
140
- let tagStAfPos;
141
- let tagStClPos;
142
- let tagStClAfPos;
143
- let tagEnPos;
144
- let tagEnClAfPos;
145
-
146
- export function getDomPosition({ domTextEditor, target, start, end }) {
147
- target = target.getAttribute('element_id');
148
- // let {tagStClAfPos} = findStartTagById(domTextEditor, target);
149
-
150
- if(findStartTagById(domTextEditor, target))
151
- findClosingTag(domTextEditor, target);
152
- else return;
153
-
154
- start = tagStClAfPos + start;
155
- end = tagStClAfPos + end;
156
- return {start, end};
157
- }
158
-
159
- export function getWholeElement(domTextEditor, target) {
160
-
161
- if(findStartTagById(domTextEditor, target)) {
162
- findClosingTag(domTextEditor, target);
163
- return { start: tagStPos, end: tagEnClAfPos || tagStClAfPos };
164
- }
165
- else
166
- return false;
167
- }
168
-
169
- export function findStartTagById(domTextEditor, target) {
170
- try {
171
- let sch = `(?:${sps}element_id\=\"${target}\"${sps})`;
172
- let reg = `(?<tagWhole>${tgs}${at}*?${sch}${at}*?${the})`;
173
- let tagStart = domTextEditor.htmlString.match(new RegExp(reg, "is"));
174
-
175
- if(!tagStart) return false;
176
- // throw new Error('element is not valid or can not be found');
177
-
178
- tagName = tagStart.groups.tagName.toUpperCase();
125
+ export function getElementPosition(str, start, end) {
126
+ let response = {};
127
+ let startString = str.substr(0, start);
128
+ let endString = str.substr(start);
129
+ let angleStart = startString.lastIndexOf("<");
130
+ let angleEnd = startString.lastIndexOf(">");
131
+ let endStringAngleEnd = endString.indexOf(">");
132
+ let element, position, nodeStart, nodeEnd, startNode, type;
133
+ if (angleEnd > angleStart) {
134
+ let length = 0;
135
+ if (start != end)
136
+ length = end - start;
137
+ let string = str.customSplice(start, length, '<findelement></findelement>');
138
+ let newDom = domParser(string);
139
+ let findEl = newDom.querySelector('findelement');
140
+ if (findEl) {
141
+ let insert = getInsertPosition(findEl);
142
+ element = insert.target;
143
+ position = insert.position;
144
+ type = 'insertAdjacent';
145
+ if(!position)
146
+ type = 'textNode';
147
+ if (type == 'textNode' || type == 'afterbegin');
148
+ nodeStart = start - angleEnd - 1;
149
+ findEl.remove();
150
+ }
151
+ }
152
+ else {
153
+ let node = str.slice(angleStart, startString.length + endStringAngleEnd + 1);
154
+ if (node.startsWith("</")) {
155
+ startNode = node.slice(0, 1) + node.slice(2);
156
+ startNode = startNode.substr(0, startNode.length - 1);
157
+ nodeStart = startString.lastIndexOf(startNode);
158
+ let endString1 = str.substr(nodeStart);
159
+ let end = endString1.indexOf(">");
160
+ nodeEnd = nodeStart + end + 1;
161
+ type = 'isEndTag';
162
+ }
163
+ else {
164
+ nodeEnd = startString.length + endStringAngleEnd + 1;
165
+ startNode = node;
166
+ nodeStart = angleStart;
167
+ type = 'isStartTag';
168
+ }
169
+ if (nodeEnd > 0) {
170
+ let string = str.customSplice(nodeEnd - 1, 0, ' findelement');
171
+ let newDom = domParser(string);
172
+ element = newDom.querySelector('[findelement]');
173
+ if (!element && newDom.tagName == 'HTML')
174
+ element = newDom;
175
+ else if (type == "isEndTag")
176
+ element = element.parentElement;
177
+ element.removeAttribute('findelement');
178
+ }
179
+ else {
180
+ let string = str.customSplice(angleStart, 0, '<findelement></findelement>');
181
+ let newDom = domParser(string);
182
+ element = newDom.querySelector('findelement');
183
+ if (element) {
184
+ let insert = getInsertPosition(element);
185
+ element = insert.target.parentElement;
186
+ position = insert.position;
187
+ if(position == 'afterend')
188
+ element = element.parentElement;
189
+ type = 'innerHTML';
190
+ }
191
+ if (!element) {
192
+ console.log('Could not find element');
193
+ }
194
+ }
195
+ }
179
196
 
180
- tagStPos = tagStart.index;
181
- tagStAfPos = tagStart.index + tagName.length + 1;
182
- tagStClPos = tagStart.index + tagStart.groups.tagWhole.length - 1 - (tagStart.groups.tagSlash ? 1 : 0);
183
- // haveClosingTag = !tagStart.groups.tagSlash;
184
- tagStClAfPos = tagStart.index + tagStart.groups.tagWhole.length;
185
- // tagNameEnd = tagStAfPos + tagName.length;
186
- // if it's like <img />
187
- if(tagStart.groups.tagSlash) {
188
- tagEnPos = tagStClPos;
189
- tagEnClAfPos = tagStClAfPos;
190
- // isOmission = true; // if the tag doesn't have closing counterpart
191
- }
192
- // return true;
193
- return {tagName, tagStPos, tagStAfPos, tagStClPos, tagStClAfPos, tagEnPos, tagEnClAfPos};
194
- } catch {
195
-
196
- }
197
- }
198
-
199
- export function findClosingTag(domTextEditor, target) {
200
- let match = domTextEditor.htmlString.substr(tagStClAfPos)
201
- .matchAll(new RegExp(`(?<tagWhole>${getEndTag(tagName)}${at}*?${the})`, 'gi'));
202
-
203
- if(!match) throw new Error('can not find any closing tag');
204
-
205
- let nest = 0;
197
+ if (element) {
198
+ response.element = element;
199
+ response.path = cssPath(element);
200
+ response.position = position;
201
+ response.start = nodeStart;
202
+ response.end = nodeEnd;
203
+ response.type = type;
204
+ }
206
205
 
207
- for(let i of match) {
208
- if(i.groups.isClosing) {
209
- if(!nest) {
210
- tagEnPos = tagStClAfPos + i.index;
211
- tagEnClAfPos = tagStClAfPos + i.index + i[0].length;
212
- // return true;
213
- return { tagEnPos, tagEnClAfPos };
214
- }
215
- else
216
- nest--;
217
- }
218
- else
219
- nest++;
220
- }
221
- throw new Error('closing tag and openning tag order does not match');
206
+ return response;
222
207
  }
223
208
 
224
- export function findElByPos(domTextEditor, pos) {
225
- let pos1 = pos - idSearch.length;
226
- let pos2 = pos + idSearch.length;
227
-
228
- pos1 = domTextEditor.htmlString.indexOf(idSearch, pos1 + idSearch.length);
229
- if(pos1 !== -1 && isPosOnEl(domTextEditor, pos1, pos))
230
- return {target, tagStClAfPos};
231
-
232
- while(true) {
233
- pos2 = domTextEditor.htmlString.lastIndexOf(idSearch, pos2 - idSearch.length);
234
-
235
- if(pos2 !== -1) {
236
- if(isPosOnEl(domTextEditor, pos2, pos))
237
- return {target, tagStClAfPos};
238
- }
239
- else return false;
240
- }
241
-
209
+ function getInsertPosition(element){
210
+ let target, position;
211
+ let previousSibling = element.previousSibling;
212
+ let nextSibling = element.nextSibling;
213
+ if (previousSibling || nextSibling) {
214
+ if (!previousSibling) {
215
+ target = element.parentElement;
216
+ position = 'afterbegin';
217
+ }
218
+ else if (!nextSibling) {
219
+ target = element.parentElement;
220
+ position = 'beforend';
221
+ }
222
+ else if (previousSibling && previousSibling.nodeType == 1) {
223
+ target = previousSibling;
224
+ position = 'afterend';
225
+ }
226
+ else if (nextSibling && nextSibling.nodeType == 1) {
227
+ target = element.parentElement;
228
+ position = 'afterbegin';
229
+ }
230
+ else {
231
+ target = element.parentElement;
232
+ }
233
+ }
234
+ else {
235
+ target = element.parentElement;
236
+ position = 'afterbegin';
237
+ }
238
+ return {target, position};
242
239
  }
243
240
 
244
- function isPosOnEl(domTextEditor, elementIdPos, pos) {
245
- target = getId(domTextEditor, elementIdPos + idSearch.length);
241
+ export function getStringPosition({string, target, position, attribute, property, value}) {
242
+ try {
243
+ let selector = cssPath(target, '[contenteditable]');
244
+ let dom = domParser(string);
245
+ let element = dom.querySelector(selector);
246
+ if (!element){
247
+ console.log('element could not be found using selector:', selector);
248
+ return;
249
+ }
250
+ let start = 0, end = 0;
251
+
252
+ if (position) {
253
+ if (position == 'beforebegin')
254
+ start = getElFromString(dom, string, element, position, true);
255
+ else
256
+ start = getElFromString(dom, string, element, position);
257
+ end = start;
258
+ }
259
+ else if (attribute) {
260
+ if (!element.hasAttribute(attribute)){
261
+ start = getElFromString(dom, string, element, 'afterbegin') - 1;
262
+ end = start;
263
+ }
264
+ else {
265
+ start = getElFromString(dom, string, element, 'beforebegin');
266
+ let elString = string.substr(start);
267
+ let attrValue = element.getAttribute(attribute);
268
+ let attrStart = elString.indexOf(` ${attribute}=`);
269
+ start = start + attrStart;
270
+ if (attribute == 'style') {
271
+ element.style[property] = value;
272
+ value = element.getAttribute(attribute);
273
+ }
274
+ else if (attribute == 'class') {
275
+ let [prop, val] = value.split(':');
276
+ if (prop && val){
277
+ if (attrValue.includes(`${prop}:`)){
278
+ let propStart = attrValue.indexOf(`${prop}:`);
279
+ let propString = attrValue.substr(propStart);
280
+ let propEnd = propString.indexOf(" ");
281
+ if (propEnd > 0)
282
+ propString = propString.slice(0, propEnd);
283
+ element.classList.remove(propString);
284
+ }
285
+ }
286
+ element.classList.add(value);
287
+ value = element.getAttribute(attribute);
288
+ }
289
+ else {
290
+ element.setAttribute(attribute, value)
291
+ value = element.getAttribute(attribute);
292
+ }
293
+ end = start + attribute.length + attrValue.length + 4;
294
+ }
295
+ }
296
+ else if (value) {
297
+ start = getElFromString(dom, string, element, 'afterbegin');
298
+ let length = element.innerHTML.length;
299
+ end = start + length;
300
+ }
301
+ else {
302
+ start = getElFromString(dom, string, element, 'beforebegin');
303
+ end = getElFromString(dom, string, element, 'afterend', true);
304
+ }
246
305
 
247
- if(!findStartTagById(domTextEditor, target))
248
- return false;
306
+ return {start, end, newValue: value};
307
+ }
308
+ catch (e){
309
+ console.log(e);
310
+ }
311
+ }
249
312
 
250
- findClosingTag(domTextEditor, target);
251
- let tagStartPos = tagStPos;
252
- let tagEndPos = tagEnClAfPos || tagStClAfPos;
313
+ function getElFromString(dom, string, element, position, wholeEl) {
314
+ let findEl = document.createElement('findelement');
315
+ let start, angle, documentTypeAngles;
316
+ if (position == 'afterbegin') {
317
+ element.insertAdjacentElement('afterbegin', findEl);
318
+ angle = '>';
319
+ }
320
+ else if (position == 'afterend') {
321
+ element.insertAdjacentElement('afterend', findEl);
322
+ angle = '>';
323
+ }
324
+ else if (position == 'beforebegin'){
325
+ element.insertAdjacentElement('afterbegin', findEl);
326
+ angle = '<';
327
+ }
328
+ else if (position == 'beforeend'){
329
+ element.insertAdjacentElement('afterend', findEl);
330
+ angle = '<';
331
+ }
332
+ if (dom.tagName == 'HTML')
333
+ start = dom.outerHTML.indexOf("<findelement></findelement>");
334
+ else
335
+ start = dom.innerHTML.indexOf("<findelement></findelement>");
336
+
337
+ findEl.remove();
338
+
339
+ let domString = dom.outerHTML.substring(0, start);
253
340
 
254
- if(pos > tagStartPos && pos < tagEndPos) {
255
- return true;
256
- }
341
+ if (dom.tagName == "HTML") {
342
+ let htmlIndex = string.indexOf('<html');
343
+ let documentType = string.substring(0, htmlIndex);
344
+ documentTypeAngles = documentType.split(angle).length -1;
345
+ }
346
+ let angles = domString.split(angle);
347
+ let angleLength = angles.length - 1;
348
+ // if (position == 'afterend' && angles[angles.length - 1] === '')
349
+ // angleLength -= 1;
350
+ // else if (position == 'afterend' && angles[angles.length - 1] !== '')
351
+ // angleLength += 1;
352
+ // if (wholeEl && position == 'afterend') {
353
+ // angleLength += 1;
354
+ // }
355
+ // if (wholeEl && position == 'beforebegin') {
356
+ // angleLength += 1;
357
+ // }
358
+ if (documentTypeAngles)
359
+ angleLength += documentTypeAngles;
360
+ let elStart = getPosition(string, angle, angleLength);
361
+
362
+ // if (position == 'beforebegin')
363
+ // elStart += 1
364
+ if (position == 'afterbegin')
365
+ elStart += 1
366
+ else if (position == 'beforeend')
367
+ elStart += 1
368
+ else if (position == 'afterend')
369
+ elStart += 1
370
+
371
+ return elStart;
257
372
  }
258
373
 
259
- function getId(domTextEditor, pos) {
260
- let attWrapper = domTextEditor.htmlString[pos];
261
- let endWrapper = domTextEditor.htmlString.indexOf(attWrapper, pos + 1);
262
- return domTextEditor.htmlString.substring(pos + 1, endWrapper);
374
+ function getPosition(string, subString, index) {
375
+ return string.split(subString, index).join(subString).length;
263
376
  }
264
377
 
265
-
266
- export default {getSelection, setSelection, hasSelection, processSelection, findElByPos, getDomPosition, getWholeElement, findStartTagById, findClosingTag};
378
+ export default {getSelection, setSelection, hasSelection, processSelection, getStringPosition, getElementPosition};