@livingdocs/editable.js 4.0.0 → 4.0.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.
package/src/selection.js CHANGED
@@ -1,5 +1,3 @@
1
-
2
- import 'rangy/lib/rangy-textrange'
3
1
  import Cursor from './cursor'
4
2
  import * as content from './content'
5
3
  import * as parser from './parser'
@@ -7,6 +5,7 @@ import * as block from './block'
7
5
  import config from './config'
8
6
  import highlightSupport from './highlight-support'
9
7
  import highlightText from './highlight-text'
8
+ import {toCharacterRange, rangeToHtml} from './util/dom'
10
9
 
11
10
  /**
12
11
  * The Selection module provides a cross-browser abstraction layer for range
@@ -37,7 +36,7 @@ export default class Selection extends Cursor {
37
36
 
38
37
  // Get the html inside the selection.
39
38
  html () {
40
- return this.range.toHtml()
39
+ return rangeToHtml(this.range)
41
40
  }
42
41
 
43
42
  isAllSelected () {
@@ -53,7 +52,7 @@ export default class Selection extends Cursor {
53
52
  }
54
53
 
55
54
  getTextRange () {
56
- return this.range.toCharacterRange(this.host)
55
+ return toCharacterRange(this.range, this.host)
57
56
  }
58
57
 
59
58
  // Return a plain string of the current selection content.
@@ -72,10 +71,9 @@ export default class Selection extends Cursor {
72
71
  getRects () {
73
72
  // consider: translate into absolute positions
74
73
  // just like Cursor#getCoordinates()
75
- return this.range.nativeRange.getClientRects()
74
+ return this.range.getClientRects()
76
75
  }
77
76
 
78
-
79
77
  link (href, attrs = {}) {
80
78
  if (href) attrs.href = href
81
79
  const link = this.createElement(config.linkMarkup.name, config.linkMarkup.attribs)
@@ -121,6 +119,18 @@ export default class Selection extends Cursor {
121
119
  }
122
120
  }
123
121
 
122
+ highlightComment ({highlightId, textRange}) {
123
+ highlightSupport.highlightRange(
124
+ this.host,
125
+ highlightId,
126
+ textRange.text,
127
+ textRange.start,
128
+ textRange.end,
129
+ undefined, // dispatcher
130
+ 'comment'
131
+ )
132
+ }
133
+
124
134
  // Manually add a highlight
125
135
  // Note: the current code does not work with newlines (LP)
126
136
  highlight ({highlightId}) {
@@ -143,12 +153,12 @@ export default class Selection extends Cursor {
143
153
  highlightText.highlightMatches(this.host, [match])
144
154
  }
145
155
 
146
- // toggle('<em>')
156
+ // e.g. toggle('<em>')
147
157
  toggle (elem) {
148
158
  if (block.isPlainTextBlock(this.host)) return
149
159
  elem = this.adoptElement(elem)
150
160
  this.range = content.toggleTag(this.host, this.range, elem)
151
- this.setSelection()
161
+ this.setVisibleSelection()
152
162
  }
153
163
 
154
164
  toggleCustom ({tagName, attributes, trim = false}) {
@@ -203,7 +213,7 @@ export default class Selection extends Cursor {
203
213
  const cursor = this.deleteContent()
204
214
  const textNode = cursor.createTextNode(character)
205
215
  cursor.insertBefore(textNode)
206
- cursor.setSelection()
216
+ cursor.setVisibleSelection()
207
217
  return cursor
208
218
  }
209
219
 
@@ -213,13 +223,13 @@ export default class Selection extends Cursor {
213
223
  // @param {String} E.g. '»'
214
224
  surround (startCharacter, endCharacter) {
215
225
  this.range = content.surround(this.host, this.range, startCharacter, endCharacter)
216
- this.setSelection()
226
+ this.setVisibleSelection()
217
227
  }
218
228
 
219
229
  removeSurround (startCharacter, endCharacter) {
220
230
  this.range = content.deleteCharacter(this.host, this.range, startCharacter)
221
231
  this.range = content.deleteCharacter(this.host, this.range, endCharacter)
222
- this.setSelection()
232
+ this.setVisibleSelection()
223
233
  }
224
234
 
225
235
  removeChars (chars = []) {
@@ -227,7 +237,7 @@ export default class Selection extends Cursor {
227
237
  const char = chars[i]
228
238
  this.range = content.deleteCharacter(this.host, this.range, char)
229
239
  }
230
- this.setSelection()
240
+ this.setVisibleSelection()
231
241
  }
232
242
 
233
243
  toggleSurround (startCharacter, endCharacter) {
@@ -244,7 +254,7 @@ export default class Selection extends Cursor {
244
254
  // remove all.
245
255
  removeFormatting (selector) {
246
256
  this.range = content.removeFormatting(this.host, this.range, selector)
247
- this.setSelection()
257
+ this.setVisibleSelection()
248
258
  }
249
259
 
250
260
  // Delete the farest ancestor that is an exact selection
@@ -284,7 +294,7 @@ export default class Selection extends Cursor {
284
294
  // @param {DOM Node}
285
295
  expandTo (elem) {
286
296
  this.range = content.expandTo(this.host, this.range, elem)
287
- this.setSelection()
297
+ this.setVisibleSelection()
288
298
  }
289
299
 
290
300
  // Collapse the selection at the beginning of the selection
@@ -292,7 +302,7 @@ export default class Selection extends Cursor {
292
302
  // @return Cursor instance
293
303
  collapseAtBeginning (elem) {
294
304
  this.range.collapse(true)
295
- this.setSelection()
305
+ this.setVisibleSelection()
296
306
  return new Cursor(this.host, this.range)
297
307
  }
298
308
 
@@ -301,7 +311,7 @@ export default class Selection extends Cursor {
301
311
  // @return Cursor instance
302
312
  collapseAtEnd (elem) {
303
313
  this.range.collapse(false)
304
- this.setSelection()
314
+ this.setVisibleSelection()
305
315
  return new Cursor(this.host, this.range)
306
316
  }
307
317
 
@@ -312,7 +322,7 @@ export default class Selection extends Cursor {
312
322
  if (block.isPlainTextBlock(this.host)) return
313
323
  elem = this.adoptElement(elem)
314
324
  this.range = content.forceWrap(this.host, this.range, elem)
315
- this.setSelection()
325
+ this.setVisibleSelection()
316
326
  }
317
327
 
318
328
  // Check if the selection is the same as the elements contents.
@@ -339,6 +349,6 @@ export default class Selection extends Cursor {
339
349
  // selection.
340
350
  deleteCharacter (character) {
341
351
  this.range = content.deleteCharacter(this.host, this.range, character)
342
- this.setSelection()
352
+ this.setVisibleSelection()
343
353
  }
344
354
  }
package/src/util/dom.js CHANGED
@@ -42,3 +42,221 @@ export const closest = (elem, selector) => {
42
42
  if (!elem.closest) elem = elem.parentNode
43
43
  if (elem && elem.closest) return elem.closest(selector)
44
44
  }
45
+
46
+ export const createRange = (win = window) => {
47
+ return win.document.createRange()
48
+ }
49
+
50
+ export const getSelection = (win = window) => {
51
+ return win.document.getSelection()
52
+ }
53
+
54
+ export const getNodes = (range, nodeTypes, filterFunc, win = window) => {
55
+ const nodes = []
56
+
57
+ const nodeIterator = win.document.createNodeIterator(
58
+ range.commonAncestorContainer,
59
+ NodeFilter.SHOW_ALL,
60
+ {
61
+ acceptNode (node) {
62
+ if (
63
+ range.intersectsNode(node) &&
64
+ nodeTypes.includes(node.nodeType) &&
65
+ node !== range.commonAncestorContainer // Exclude the common ancestor container
66
+ ) {
67
+ if (typeof filterFunc === 'function') {
68
+ return filterFunc(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
69
+ }
70
+ return NodeFilter.FILTER_ACCEPT
71
+ }
72
+ return NodeFilter.FILTER_SKIP
73
+ }
74
+ },
75
+ false
76
+ )
77
+
78
+ let currentNode
79
+ while ((currentNode = nodeIterator.nextNode())) {
80
+ nodes.push(currentNode)
81
+ }
82
+
83
+ return nodes
84
+ }
85
+
86
+ // export const getNodes = (range, nodeTypes, filterFunc, win = window) => {
87
+ // const nodes = []
88
+
89
+ // const nodeIterator = win.document.createNodeIterator(
90
+ // range.startContainer,
91
+ // NodeFilter.SHOW_ALL,
92
+ // {
93
+ // acceptNode (node) {
94
+ // if (
95
+ // range.intersectsNode(node) &&
96
+ // nodeTypes.includes(node.nodeType)
97
+ // ) {
98
+ // if (typeof filterFunc === 'function') {
99
+ // return filterFunc(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
100
+ // }
101
+ // return NodeFilter.FILTER_ACCEPT
102
+ // }
103
+ // return NodeFilter.FILTER_SKIP
104
+ // }
105
+ // },
106
+ // false
107
+ // )
108
+
109
+ // let currentNode
110
+ // while ((currentNode = nodeIterator.nextNode())) {
111
+ // if (range.intersectsNode(currentNode)) {
112
+ // nodes.push(currentNode)
113
+ // }
114
+ // }
115
+
116
+ // return nodes
117
+ // }
118
+
119
+
120
+ export const normalizeBoundaries = (range) => {
121
+ if (range.startContainer.compareDocumentPosition(range.endContainer) === Node.DOCUMENT_POSITION_FOLLOWING) {
122
+ range.setStartBefore(range.endContainer)
123
+ }
124
+
125
+ if (range.endContainer.compareDocumentPosition(range.startContainer) === Node.DOCUMENT_POSITION_PRECEDING) {
126
+ range.setEndAfter(range.startContainer)
127
+ }
128
+ }
129
+
130
+ export const containsRange = (containerRange, testRange) => {
131
+ return (
132
+ containerRange.compareBoundaryPoints(Range.START_TO_START, testRange) <= 0 &&
133
+ containerRange.compareBoundaryPoints(Range.END_TO_END, testRange) >= 0
134
+ )
135
+ }
136
+
137
+ export const containsNodeText = (range, node) => {
138
+ const nodeRange = document.createRange()
139
+ nodeRange.selectNodeContents(node)
140
+ const comparisonStart = range.compareBoundaryPoints(Range.START_TO_START, nodeRange)
141
+ const comparisonEnd = range.compareBoundaryPoints(Range.END_TO_END, nodeRange)
142
+ return comparisonStart <= 0 && comparisonEnd >= 0
143
+ }
144
+
145
+ export const nodeContainsRange = (node, range) => {
146
+ const nodeRange = document.createRange()
147
+ nodeRange.selectNodeContents(node)
148
+ const comparisonStart = range.compareBoundaryPoints(Range.START_TO_START, nodeRange)
149
+ const comparisonEnd = range.compareBoundaryPoints(Range.END_TO_END, nodeRange)
150
+ return comparisonStart >= 0 && comparisonEnd <= 0
151
+ }
152
+
153
+ const isCharacterDataNode = (node) => {
154
+ return node && (node.nodeType === Node.TEXT_NODE || node.nodeType === Node.COMMENT_NODE)
155
+ }
156
+
157
+ const splitDataNode = (node, offset) => {
158
+ return node.splitText(offset)
159
+ }
160
+
161
+ export const splitBoundaries = (range) => {
162
+ const startContainer = range.startContainer
163
+ const startOffset = range.startOffset
164
+ const endContainer = range.endContainer
165
+ const endOffset = range.endOffset
166
+
167
+ if (isCharacterDataNode(endContainer) && endOffset > 0 && endOffset < endContainer.length) {
168
+ splitDataNode(endContainer, endOffset)
169
+ }
170
+
171
+ if (isCharacterDataNode(startContainer) && startOffset > 0 && startOffset < startContainer.length) {
172
+ const newStartContainer = splitDataNode(startContainer, startOffset)
173
+ range.setStart(newStartContainer, 0)
174
+ }
175
+ }
176
+
177
+ export const toCharacterRange = (range, container) => {
178
+ const startRange = range.cloneRange()
179
+ startRange.setStart(container, 0)
180
+ startRange.setEnd(range.startContainer, range.startOffset)
181
+
182
+ const rangeText = range.toString()
183
+ const start = startRange.toString().length
184
+ const end = start + rangeText.length
185
+
186
+ return {start, end, text: rangeText}
187
+ }
188
+
189
+ export const rangesAreEqual = (range1, range2) => {
190
+ return (
191
+ range1.startContainer === range2.startContainer &&
192
+ range1.startOffset === range2.startOffset &&
193
+ range1.endContainer === range2.endContainer &&
194
+ range1.endOffset === range2.endOffset
195
+ )
196
+ }
197
+
198
+ export const rangeToHtml = (range, win = window) => {
199
+ const div = win.document.createElement('div')
200
+ div.appendChild(range.cloneContents())
201
+ return div.innerHTML
202
+ }
203
+
204
+ export const getSelectionCoordinates = (selection) => {
205
+ const range = selection.getRangeAt(0) // Assuming you want coordinates of the first range
206
+
207
+ const rects = range.getClientRects()
208
+ const coordinates = []
209
+
210
+ for (let i = 0; i < rects.length; i++) {
211
+ const rect = rects[i]
212
+ coordinates.push({
213
+ top: rect.top,
214
+ left: rect.left,
215
+ bottom: rect.bottom,
216
+ right: rect.right,
217
+ width: rect.width,
218
+ height: rect.height
219
+ })
220
+ }
221
+
222
+ return coordinates
223
+ }
224
+
225
+ export const createRangeFromCharacterRange = (element, actualStartIndex, actualEndIndex) => {
226
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false)
227
+ let currentIndex = 0
228
+ let startNode, endNode, startOffset, endOffset
229
+
230
+ while (walker.nextNode()) {
231
+ const textNode = walker.currentNode
232
+ const nodeLength = textNode.nodeValue.length
233
+
234
+ if (currentIndex + nodeLength <= actualStartIndex) {
235
+ currentIndex += nodeLength
236
+ continue
237
+ }
238
+
239
+ if (!startNode) {
240
+ startNode = textNode
241
+ startOffset = actualStartIndex - currentIndex
242
+ }
243
+
244
+ if (currentIndex + nodeLength >= actualEndIndex) {
245
+ endNode = textNode
246
+ endOffset = actualEndIndex - currentIndex
247
+ break
248
+ }
249
+
250
+ currentIndex += nodeLength
251
+ }
252
+
253
+ if (startNode && endNode) {
254
+ const range = createRange()
255
+ range.setStart(startNode, startOffset)
256
+ range.setEnd(endNode, endOffset)
257
+ return range
258
+ } else {
259
+ throw new Error('Invalid character offsets.')
260
+ }
261
+ }
262
+
@@ -1,102 +0,0 @@
1
- export default class WordHighlighting {
2
-
3
- // @param markerNode {DOM Node}
4
- // @param matchMode {String} either 'word' or 'text'
5
- constructor (markerNode, matchMode = 'word') {
6
- this.marker = markerNode
7
- this.matchMode = matchMode
8
- }
9
-
10
- isElement (obj) {
11
- try {
12
- if (!obj) return false
13
- return obj instanceof obj.ownerDocument?.defaultView.HTMLElement
14
- } catch (e) {
15
- // Browsers not supporting W3 DOM2 don't have HTMLElement and
16
- // an exception is thrown and we end up here. Testing some
17
- // properties that all elements have (works on IE7)
18
- return (typeof obj === 'object') &&
19
- (obj.nodeType === 1) && (typeof obj.style === 'object') &&
20
- (typeof obj.ownerDocument === 'object')
21
- }
22
- }
23
-
24
- findMatches (text, highlights) {
25
- if (!text || text === '' || !this.isElement(this.marker)) return
26
-
27
- if (highlights && highlights.length > 0) {
28
- return this.searchMatches(text, highlights)
29
- }
30
- }
31
-
32
- searchMatches (text, highlights) {
33
- const createRegex = this.matchMode === 'word'
34
- ? createWordRegex
35
- : createHighlightRegex
36
-
37
- const regex = createRegex(highlights)
38
- const matches = [...text.matchAll(regex)]
39
-
40
- return matches.map((entry) => this.prepareMatch(entry))
41
- }
42
-
43
- prepareMatch (match) {
44
- let startIndex
45
- let matchedText
46
- if (this.matchMode === 'word') {
47
- startIndex = match.index + match[1].length
48
- matchedText = match[2]
49
- } else {
50
- startIndex = match.index
51
- matchedText = match[0]
52
- }
53
-
54
- return {
55
- startIndex,
56
- endIndex: startIndex + matchedText.length,
57
- match: matchedText,
58
- marker: this.marker
59
- }
60
- }
61
-
62
- }
63
-
64
- function createHighlightRegex (words) {
65
- const escapedWords = (words || []).map((word) => escapeRegEx(word))
66
-
67
- const regex = `(${escapedWords.join('|')})`
68
- return new RegExp(regex, 'g')
69
- }
70
-
71
- // Regex to find whole words within a string
72
- //
73
- // Unicode character blocks for letters.
74
- // See: http://jrgraphix.net/research/unicode_blocks.php
75
- //
76
- // \\u0041-\\u005A A-Z (Basic Latin)
77
- // \\u0061-\\u007A a-z (Basic Latin)
78
- // \\u0030-\\u0039 0-9 (Basic Latin)
79
- // \\u00AA ª (Latin-1 Supplement)
80
- // \\u00B5 µ (Latin-1 Supplement)
81
- // \\u00BA º (Latin-1 Supplement)
82
- // \\u00C0-\\u00D6 À-Ö (Latin-1 Supplement)
83
- // \\u00D8-\\u00F6 Ø-ö (Latin-1 Supplement)
84
- // \\u00F8-\\u00FF ø-ÿ (Latin-1 Supplement)
85
- // \\u0100-\\u017F Ā-ſ (Latin Extended-A)
86
- // \\u0180-\\u024F ƀ-ɏ (Latin Extended-B)
87
- const letterChars = '\\u0041-\\u005A\\u0061-\\u007A\\u0030-\\u0039\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u00FF\\u0100-\\u017F\\u0180-\\u024F'
88
-
89
- function createWordRegex (words) {
90
- const escapedWords = (words || []).map((word) => escapeRegEx(word))
91
-
92
- // (notLetter|^)(words)(?=notLetter|$)
93
- const regex = `([^${letterChars}]|^)` +
94
- `(${escapedWords.join('|')})` +
95
- `(?=[^${letterChars}]|$)`
96
-
97
- return new RegExp(regex, 'g')
98
- }
99
-
100
- function escapeRegEx (s) {
101
- return String(s).replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1')
102
- }