@livingdocs/editable.js 4.0.1 → 5.0.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/dist/editable.js +1 -1
- package/dist/editable.js.map +1 -1
- package/package.json +5 -7
- package/src/content.js +92 -20
- package/src/core.js +10 -23
- package/src/cursor.js +20 -14
- package/src/dispatcher.js +8 -8
- package/src/feature-detection.js +7 -5
- package/src/highlight-support.js +38 -57
- package/src/highlight-text.js +20 -12
- package/src/keyboard.js +6 -7
- package/src/{highlighting.js → monitored-highlighting.js} +20 -15
- package/src/plugins/highlighting/match-collection.js +2 -3
- package/src/plugins/highlighting/text-search.js +100 -0
- package/src/plugins/highlighting/whitespace-highlighting.js +2 -7
- package/src/range-container.js +6 -5
- package/src/range-save-restore.js +26 -33
- package/src/selection-watcher.js +16 -17
- package/src/selection.js +28 -18
- package/src/util/dom.js +218 -0
- package/src/plugins/highlighting/text-highlighting.js +0 -102
package/package.json
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livingdocs/editable.js",
|
|
3
3
|
"description": "Friendly contenteditable API",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "5.0.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@babel/runtime": "^7.16.7",
|
|
7
|
-
"
|
|
8
|
-
"lodash-es": "^4.17.21",
|
|
9
|
-
"rangy": "^1.3.0"
|
|
7
|
+
"lodash-es": "^4.17.21"
|
|
10
8
|
},
|
|
11
9
|
"devDependencies": {
|
|
12
10
|
"@babel/cli": "^7.16.8",
|
|
@@ -82,11 +80,11 @@
|
|
|
82
80
|
"isCi": true,
|
|
83
81
|
"name": "Drone",
|
|
84
82
|
"service": "drone",
|
|
85
|
-
"commit": "
|
|
86
|
-
"build": "
|
|
83
|
+
"commit": "6be0ace70ff4180dfe612c8026ecfb9a88a92651",
|
|
84
|
+
"build": "1333",
|
|
87
85
|
"branch": "master",
|
|
88
86
|
"isPr": false,
|
|
89
87
|
"slug": "livingdocsIO/editable.js",
|
|
90
|
-
"date": "2023-10-
|
|
88
|
+
"date": "2023-10-20T10:13:40.826Z"
|
|
91
89
|
}
|
|
92
90
|
}
|
package/src/content.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import rangy from 'rangy'
|
|
2
1
|
import * as nodeType from './node-type'
|
|
3
2
|
import * as rangeSaveRestore from './range-save-restore'
|
|
4
3
|
import * as parser from './parser'
|
|
5
4
|
import * as string from './util/string'
|
|
6
|
-
import {createElement} from './util/dom'
|
|
5
|
+
import {createElement, createRange, getNodes, normalizeBoundaries, splitBoundaries, containsNodeText} from './util/dom'
|
|
7
6
|
import config from './config'
|
|
8
7
|
|
|
9
8
|
function restoreRange (host, range, func) {
|
|
@@ -129,11 +128,10 @@ export function adoptElement (node, doc) {
|
|
|
129
128
|
: node
|
|
130
129
|
}
|
|
131
130
|
|
|
132
|
-
// This is a slight variation of the cloneContents method of a rangyRange.
|
|
133
131
|
// It will return a fragment with the cloned contents of the range
|
|
134
132
|
// without the commonAncestorElement.
|
|
135
133
|
//
|
|
136
|
-
// @param {
|
|
134
|
+
// @param {Range}
|
|
137
135
|
// @return {DocumentFragment}
|
|
138
136
|
export function cloneRangeContents (range) {
|
|
139
137
|
const rangeFragment = range.cloneContents()
|
|
@@ -241,13 +239,13 @@ export function areSameAttributes (attrs1, attrs2) {
|
|
|
241
239
|
|
|
242
240
|
// Get all tags that start or end inside the range
|
|
243
241
|
export function getInnerTags (range, filterFunc) {
|
|
244
|
-
return
|
|
242
|
+
return getNodes(range, [nodeType.elementNode], filterFunc)
|
|
245
243
|
}
|
|
246
244
|
|
|
247
245
|
// Get all tags whose text is completely within the current selection.
|
|
248
246
|
export function getContainedTags (range, filterFunc) {
|
|
249
|
-
return
|
|
250
|
-
.filter(elem =>
|
|
247
|
+
return getNodes(range, [nodeType.elementNode], filterFunc)
|
|
248
|
+
.filter(elem => containsNodeText(range, elem))
|
|
251
249
|
}
|
|
252
250
|
|
|
253
251
|
// Transform an array of elements into an array
|
|
@@ -265,21 +263,26 @@ export function isAffectedBy (host, range, tagName) {
|
|
|
265
263
|
|
|
266
264
|
// select a whole element
|
|
267
265
|
export function selectNodeContents (element) {
|
|
268
|
-
const range =
|
|
266
|
+
const range = createRange()
|
|
269
267
|
range.selectNodeContents(element)
|
|
270
268
|
return range
|
|
271
269
|
}
|
|
272
270
|
|
|
271
|
+
function intersectsRange (range1, range2) {
|
|
272
|
+
return range1.compareBoundaryPoints(Range.END_TO_START, range2) === -1 &&
|
|
273
|
+
range2.compareBoundaryPoints(Range.END_TO_START, range1) === -1
|
|
274
|
+
}
|
|
275
|
+
|
|
273
276
|
// Check if the range selects all of the elements contents,
|
|
274
277
|
// not less or more.
|
|
275
278
|
//
|
|
276
279
|
// @param visible: Only compare visible text. That way it does not
|
|
277
280
|
// matter if the user selects an additional whitespace or not.
|
|
278
281
|
export function isExactSelection (range, elem, visible) {
|
|
279
|
-
const elemRange =
|
|
282
|
+
const elemRange = createRange()
|
|
280
283
|
elemRange.selectNodeContents(elem)
|
|
281
284
|
|
|
282
|
-
if (!
|
|
285
|
+
if (!intersectsRange(range, elemRange)) return false
|
|
283
286
|
|
|
284
287
|
let rangeText = range.toString()
|
|
285
288
|
let elemText = (elem.jquery ? elem[0] : elem).textContent
|
|
@@ -309,7 +312,7 @@ export function toggleTag (host, range, elem) {
|
|
|
309
312
|
}
|
|
310
313
|
|
|
311
314
|
export function isWrappable (range) {
|
|
312
|
-
return
|
|
315
|
+
return canSurroundContents(range)
|
|
313
316
|
}
|
|
314
317
|
|
|
315
318
|
export function forceWrap (host, range, elem) {
|
|
@@ -384,11 +387,10 @@ export function nukeElem (host, range, node) {
|
|
|
384
387
|
export function insertCharacter (range, character, atStart) {
|
|
385
388
|
const insertEl = document.createTextNode(character)
|
|
386
389
|
const boundaryRange = range.cloneRange()
|
|
387
|
-
|
|
388
390
|
boundaryRange.collapse(atStart)
|
|
389
391
|
boundaryRange.insertNode(insertEl)
|
|
390
392
|
range[atStart ? 'setStartBefore' : 'setEndAfter'](insertEl)
|
|
391
|
-
|
|
393
|
+
normalizeBoundaries(range)
|
|
392
394
|
}
|
|
393
395
|
|
|
394
396
|
// Surround the range with characters like start and end quotes.
|
|
@@ -406,19 +408,18 @@ export function surround (host, range, startCharacter, endCharacter) {
|
|
|
406
408
|
export function deleteCharacter (host, range, character) {
|
|
407
409
|
if (!containsString(range, character)) return range
|
|
408
410
|
|
|
409
|
-
|
|
411
|
+
// check for selection.rangeCount > 0 ?
|
|
412
|
+
if (window.getSelection().rangeCount > 0) splitBoundaries(range)
|
|
410
413
|
const restoredRange = restoreRange(host, range, () => {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
range.getNodes([nodeType.textNode], (node) => {
|
|
414
|
-
return node.nodeValue.search(charRegexp) >= 0
|
|
414
|
+
getNodes(range, [nodeType.textNode], (node) => {
|
|
415
|
+
return node.nodeValue.indexOf(character) >= 0
|
|
415
416
|
})
|
|
416
417
|
.forEach((node) => {
|
|
417
|
-
node.nodeValue = node.nodeValue.
|
|
418
|
+
node.nodeValue = node.nodeValue.replaceAll(character, '')
|
|
418
419
|
})
|
|
419
420
|
})
|
|
420
421
|
|
|
421
|
-
|
|
422
|
+
normalizeBoundaries(restoredRange)
|
|
422
423
|
return restoredRange
|
|
423
424
|
}
|
|
424
425
|
|
|
@@ -433,3 +434,74 @@ export function nukeTag (host, range, tagName) {
|
|
|
433
434
|
if (elem.nodeName.toUpperCase() === tagName.toUpperCase()) unwrap(elem)
|
|
434
435
|
})
|
|
435
436
|
}
|
|
437
|
+
|
|
438
|
+
function createNodeIterator (root, filter) {
|
|
439
|
+
let currentNode = root
|
|
440
|
+
let previousNode = null
|
|
441
|
+
|
|
442
|
+
function nextNode () {
|
|
443
|
+
if (!currentNode) {
|
|
444
|
+
return null
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (currentNode.firstChild && previousNode !== currentNode.firstChild) {
|
|
448
|
+
previousNode = currentNode
|
|
449
|
+
currentNode = currentNode.firstChild
|
|
450
|
+
} else if (currentNode.nextSibling) {
|
|
451
|
+
previousNode = currentNode
|
|
452
|
+
currentNode = currentNode.nextSibling
|
|
453
|
+
} else {
|
|
454
|
+
let parent = currentNode.parentNode
|
|
455
|
+
while (parent && parent !== root) {
|
|
456
|
+
if (parent.nextSibling) {
|
|
457
|
+
previousNode = currentNode = parent.nextSibling
|
|
458
|
+
break
|
|
459
|
+
}
|
|
460
|
+
parent = parent.parentNode
|
|
461
|
+
}
|
|
462
|
+
if (!parent || parent === root) {
|
|
463
|
+
previousNode = currentNode = null
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return currentNode
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
next: nextNode
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function isNodeFullyContained (node, range) {
|
|
476
|
+
const nodeRange = document.createRange()
|
|
477
|
+
nodeRange.selectNodeContents(node)
|
|
478
|
+
return range.compareBoundaryPoints(Range.START_TO_START, nodeRange) <= 0 &&
|
|
479
|
+
range.compareBoundaryPoints(Range.END_TO_END, nodeRange) >= 0
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function canSurroundContents (range) {
|
|
483
|
+
if (!range || !range.startContainer || !range.endContainer) {
|
|
484
|
+
return false
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (range.startContainer === range.endContainer) return true
|
|
488
|
+
|
|
489
|
+
// Create a custom node iterator for the common ancestor container
|
|
490
|
+
const iterator = createNodeIterator(range.commonAncestorContainer, function (node) {
|
|
491
|
+
return range.isPointInRange(node, 0)
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
let currentNode
|
|
495
|
+
let boundariesInvalid = false
|
|
496
|
+
|
|
497
|
+
while ((currentNode = iterator.next())) {
|
|
498
|
+
if (currentNode.nodeType === Node.ELEMENT_NODE) {
|
|
499
|
+
if (!isNodeFullyContained(currentNode, range)) {
|
|
500
|
+
boundariesInvalid = true
|
|
501
|
+
break
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return !boundariesInvalid
|
|
507
|
+
}
|
package/src/core.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import rangy from 'rangy'
|
|
2
1
|
import config from './config'
|
|
3
2
|
import error from './util/error'
|
|
4
3
|
import * as parser from './parser'
|
|
@@ -8,12 +7,11 @@ import * as clipboard from './clipboard'
|
|
|
8
7
|
import Dispatcher from './dispatcher'
|
|
9
8
|
import Cursor from './cursor'
|
|
10
9
|
import highlightSupport from './highlight-support'
|
|
11
|
-
import
|
|
10
|
+
import MonitoredHighlighting from './monitored-highlighting'
|
|
12
11
|
import createDefaultEvents from './create-default-events'
|
|
13
|
-
import browser from 'bowser'
|
|
14
12
|
import {textNodesUnder, getTextNodeAndRelativeOffset} from './util/element'
|
|
15
13
|
import {binaryCursorSearch} from './util/binary_search'
|
|
16
|
-
import {domArray} from './util/dom'
|
|
14
|
+
import {domArray, createRange, nodeContainsRange} from './util/dom'
|
|
17
15
|
|
|
18
16
|
/**
|
|
19
17
|
* The Core module provides the Editable class that defines the Editable.JS
|
|
@@ -49,10 +47,6 @@ export class Editable {
|
|
|
49
47
|
this.win = this.config.window
|
|
50
48
|
this.editableSelector = `.${config.editableClass}`
|
|
51
49
|
|
|
52
|
-
if (!rangy.initialized) {
|
|
53
|
-
rangy.init()
|
|
54
|
-
}
|
|
55
|
-
|
|
56
50
|
this.dispatcher = new Dispatcher(this)
|
|
57
51
|
if (this.config.defaultBehavior === true) {
|
|
58
52
|
this.dispatcher.on(createDefaultEvents(this))
|
|
@@ -217,7 +211,7 @@ export class Editable {
|
|
|
217
211
|
const host = Cursor.findHost(element, this.editableSelector)
|
|
218
212
|
if (!host) return undefined
|
|
219
213
|
|
|
220
|
-
const range =
|
|
214
|
+
const range = createRange()
|
|
221
215
|
|
|
222
216
|
if (position === 'beginning' || position === 'end') {
|
|
223
217
|
range.selectNodeContents(element)
|
|
@@ -240,7 +234,7 @@ export class Editable {
|
|
|
240
234
|
createCursorAtCharacterOffset ({element, offset}) {
|
|
241
235
|
const textNodes = textNodesUnder(element)
|
|
242
236
|
const {node, relativeOffset} = getTextNodeAndRelativeOffset({textNodes, absOffset: offset})
|
|
243
|
-
const newRange =
|
|
237
|
+
const newRange = createRange()
|
|
244
238
|
newRange.setStart(node, relativeOffset)
|
|
245
239
|
newRange.collapse(true)
|
|
246
240
|
|
|
@@ -320,21 +314,15 @@ export class Editable {
|
|
|
320
314
|
* @returns A Cursor or Selection object or undefined.
|
|
321
315
|
*/
|
|
322
316
|
getSelection (editableHost) {
|
|
323
|
-
|
|
324
|
-
if (!
|
|
317
|
+
const selection = this.dispatcher.selectionWatcher.getFreshSelection()
|
|
318
|
+
if (!editableHost || !selection) return selection
|
|
325
319
|
|
|
326
320
|
const range = selection.range
|
|
321
|
+
|
|
327
322
|
// Check if the selection is inside the editableHost
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
if (range.compareNode(editableHost) !== range.NODE_BEFORE_AND_AFTER) {
|
|
331
|
-
selection = undefined
|
|
332
|
-
}
|
|
333
|
-
} catch (e) {
|
|
334
|
-
selection = undefined
|
|
323
|
+
if (editableHost?.isConnected && nodeContainsRange(editableHost, range)) {
|
|
324
|
+
return selection
|
|
335
325
|
}
|
|
336
|
-
|
|
337
|
-
return selection
|
|
338
326
|
}
|
|
339
327
|
|
|
340
328
|
/**
|
|
@@ -343,7 +331,7 @@ export class Editable {
|
|
|
343
331
|
* @chainable
|
|
344
332
|
*/
|
|
345
333
|
setupHighlighting (hightlightingConfig) {
|
|
346
|
-
this.highlighting = new
|
|
334
|
+
this.highlighting = new MonitoredHighlighting(this, hightlightingConfig)
|
|
347
335
|
|
|
348
336
|
return this
|
|
349
337
|
}
|
|
@@ -524,7 +512,6 @@ export class Editable {
|
|
|
524
512
|
// Expose modules and editable
|
|
525
513
|
Editable.parser = parser
|
|
526
514
|
Editable.content = content
|
|
527
|
-
Editable.browser = browser
|
|
528
515
|
|
|
529
516
|
// Set up callback functions for several events.
|
|
530
517
|
;['focus', 'blur', 'flow', 'selection', 'cursor', 'newline',
|
package/src/cursor.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import rangy from 'rangy'
|
|
2
1
|
import * as viewport from './util/viewport'
|
|
3
2
|
|
|
4
3
|
import * as content from './content'
|
|
@@ -8,7 +7,7 @@ import {elementNode, documentFragmentNode} from './node-type'
|
|
|
8
7
|
import error from './util/error'
|
|
9
8
|
import * as rangeSaveRestore from './range-save-restore'
|
|
10
9
|
// import printRange from './util/print_range'
|
|
11
|
-
import {closest} from './util/dom'
|
|
10
|
+
import {closest, getSelection, rangesAreEqual} from './util/dom'
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* The Cursor module provides a cross-browser abstraction layer for cursor.
|
|
@@ -29,9 +28,9 @@ export default class Cursor {
|
|
|
29
28
|
* @class Cursor
|
|
30
29
|
* @constructor
|
|
31
30
|
*/
|
|
32
|
-
constructor (editableHost,
|
|
31
|
+
constructor (editableHost, range) {
|
|
33
32
|
this.setHost(editableHost)
|
|
34
|
-
this.range =
|
|
33
|
+
this.range = range
|
|
35
34
|
this.isCursor = true
|
|
36
35
|
}
|
|
37
36
|
|
|
@@ -100,7 +99,7 @@ export default class Cursor {
|
|
|
100
99
|
hostRange.selectNodeContents(this.host)
|
|
101
100
|
hostRange.collapse(false)
|
|
102
101
|
const hostCoords = getRangeBoundingClientRect(hostRange, this.win)
|
|
103
|
-
const cursorCoords = getRangeBoundingClientRect(this.range
|
|
102
|
+
const cursorCoords = getRangeBoundingClientRect(this.range, this.win)
|
|
104
103
|
return isCloseTo(hostCoords.bottom, cursorCoords.bottom)
|
|
105
104
|
}
|
|
106
105
|
|
|
@@ -109,7 +108,7 @@ export default class Cursor {
|
|
|
109
108
|
hostRange.selectNodeContents(this.host)
|
|
110
109
|
hostRange.collapse(true)
|
|
111
110
|
const hostCoords = getRangeBoundingClientRect(hostRange, this.win)
|
|
112
|
-
const cursorCoords = getRangeBoundingClientRect(this.range
|
|
111
|
+
const cursorCoords = getRangeBoundingClientRect(this.range, this.win)
|
|
113
112
|
return isCloseTo(hostCoords.top, cursorCoords.top)
|
|
114
113
|
}
|
|
115
114
|
|
|
@@ -139,6 +138,7 @@ export default class Cursor {
|
|
|
139
138
|
this.range.insertNode(element)
|
|
140
139
|
this.range.setStartAfter(preceedingElement)
|
|
141
140
|
this.range.setEndAfter(preceedingElement)
|
|
141
|
+
this.host.normalize() // mend text nodes
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
// Insert content after the cursor
|
|
@@ -149,7 +149,12 @@ export default class Cursor {
|
|
|
149
149
|
if (parser.isDocumentFragmentWithoutChildren(element)) return
|
|
150
150
|
|
|
151
151
|
element = this.adoptElement(element)
|
|
152
|
-
|
|
152
|
+
|
|
153
|
+
const after = this.range.cloneRange()
|
|
154
|
+
after.setStart(after.endContainer, after.endOffset)
|
|
155
|
+
after.collapse(true)
|
|
156
|
+
after.insertNode(element)
|
|
157
|
+
this.host.normalize() // mend text nodes
|
|
153
158
|
}
|
|
154
159
|
|
|
155
160
|
// Alias for #setVisibleSelection()
|
|
@@ -162,7 +167,10 @@ export default class Cursor {
|
|
|
162
167
|
const {x, y} = viewport.getScrollPosition(this.win)
|
|
163
168
|
this.win.scrollTo(x, y)
|
|
164
169
|
}
|
|
165
|
-
|
|
170
|
+
|
|
171
|
+
const selection = getSelection(this.win)
|
|
172
|
+
selection.removeAllRanges()
|
|
173
|
+
selection.addRange(this.range)
|
|
166
174
|
}
|
|
167
175
|
|
|
168
176
|
// Take the following example:
|
|
@@ -218,17 +226,16 @@ export default class Cursor {
|
|
|
218
226
|
}
|
|
219
227
|
|
|
220
228
|
getBoundingClientRect () {
|
|
221
|
-
return this.range.
|
|
229
|
+
return this.range.getBoundingClientRect()
|
|
222
230
|
}
|
|
223
231
|
|
|
224
232
|
// Get the BoundingClientRect of the cursor.
|
|
225
233
|
// The returned values are transformed to be absolute
|
|
226
234
|
// (relative to the document).
|
|
227
235
|
getCoordinates (positioning = 'absolute') {
|
|
228
|
-
const coords = this.range.
|
|
236
|
+
const coords = this.range.getBoundingClientRect()
|
|
229
237
|
if (positioning === 'fixed') return coords
|
|
230
238
|
|
|
231
|
-
|
|
232
239
|
// translate into absolute positions
|
|
233
240
|
const {x, y} = viewport.getScrollPosition(this.win)
|
|
234
241
|
return {
|
|
@@ -315,7 +322,7 @@ export default class Cursor {
|
|
|
315
322
|
if (!cursor.host.isEqualNode(this.host)) return false
|
|
316
323
|
|
|
317
324
|
if (!cursor.range) return false
|
|
318
|
-
if (!cursor.range
|
|
325
|
+
if (!rangesAreEqual(cursor.range, this.range)) return false
|
|
319
326
|
|
|
320
327
|
return true
|
|
321
328
|
}
|
|
@@ -345,8 +352,7 @@ export default class Cursor {
|
|
|
345
352
|
// This is to prevent excessive triggering of the change event during
|
|
346
353
|
// merge or split operations or other manipulations by scripts.
|
|
347
354
|
triggerChange () {
|
|
348
|
-
const event =
|
|
349
|
-
event.initEvent('formatEditable', true, false)
|
|
355
|
+
const event = new Event('formatEditable', {bubbles: true, cancelable: false})
|
|
350
356
|
this.host.dispatchEvent(event)
|
|
351
357
|
}
|
|
352
358
|
}
|
package/src/dispatcher.js
CHANGED
|
@@ -252,10 +252,10 @@ export default class Dispatcher {
|
|
|
252
252
|
})
|
|
253
253
|
|
|
254
254
|
.on('backspace', function (event) {
|
|
255
|
-
const
|
|
256
|
-
if (!
|
|
255
|
+
const rangeContainer = self.selectionWatcher.getFreshRange()
|
|
256
|
+
if (!rangeContainer.isCursor) return self.triggerChangeEvent(this)
|
|
257
257
|
|
|
258
|
-
const cursor =
|
|
258
|
+
const cursor = rangeContainer.getCursor()
|
|
259
259
|
if (!cursor.isAtBeginning()) return self.triggerChangeEvent(this)
|
|
260
260
|
|
|
261
261
|
event.preventDefault()
|
|
@@ -264,10 +264,10 @@ export default class Dispatcher {
|
|
|
264
264
|
})
|
|
265
265
|
|
|
266
266
|
.on('delete', function (event) {
|
|
267
|
-
const
|
|
268
|
-
if (!
|
|
267
|
+
const rangeContainer = self.selectionWatcher.getFreshRange()
|
|
268
|
+
if (!rangeContainer.isCursor) return self.triggerChangeEvent(this)
|
|
269
269
|
|
|
270
|
-
const cursor =
|
|
270
|
+
const cursor = rangeContainer.getCursor()
|
|
271
271
|
if (!cursor.isAtTextEnd()) return self.triggerChangeEvent(this)
|
|
272
272
|
|
|
273
273
|
event.preventDefault()
|
|
@@ -278,8 +278,8 @@ export default class Dispatcher {
|
|
|
278
278
|
.on('enter', function (event) {
|
|
279
279
|
event.preventDefault()
|
|
280
280
|
event.stopPropagation()
|
|
281
|
-
const
|
|
282
|
-
const cursor =
|
|
281
|
+
const rangeContainer = self.selectionWatcher.getFreshRange()
|
|
282
|
+
const cursor = rangeContainer.forceCursor()
|
|
283
283
|
|
|
284
284
|
if (cursor.isAtTextEnd()) {
|
|
285
285
|
self.notify('insert', this, 'after', cursor)
|
package/src/feature-detection.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import browser from 'bowser'
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Check for contenteditable support
|
|
5
3
|
*
|
|
@@ -10,9 +8,13 @@ import browser from 'bowser'
|
|
|
10
8
|
*/
|
|
11
9
|
export const contenteditable = typeof document.documentElement.contentEditable !== 'undefined'
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
// Detect webkit browser engine
|
|
12
|
+
// That way we can detect the contenteditable span bug on safari, but exclude chrome
|
|
13
|
+
// Regex taken from: https://github.com/lancedikson/bowser/blob/f09411489ced05811c91cc6670a8e4ca9cbe39a7/src/parser-engines.js#L93-L106
|
|
14
|
+
// Attention, this might be error prone as any engine version change breaks this.
|
|
15
|
+
const isBlink = /(apple)?webkit\/537\.36/i.test(window.navigator.userAgent)
|
|
16
|
+
const isWebkit = /(apple)?webkit/i.test(window.navigator.userAgent)
|
|
17
|
+
const webKit = !isBlink && isWebkit
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* Check selectionchange event (supported in IE, Chrome, Firefox and Safari)
|
package/src/highlight-support.js
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
import rangy from 'rangy'
|
|
2
1
|
import * as content from './content'
|
|
3
2
|
import highlightText from './highlight-text'
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
function isInHost (elem, host) {
|
|
8
|
-
return closest(elem, '[data-editable]:not([data-word-id])') === host
|
|
9
|
-
}
|
|
3
|
+
import {searchText} from './plugins/highlighting/text-search'
|
|
4
|
+
import {createElement, createRange, toCharacterRange} from './util/dom'
|
|
10
5
|
|
|
11
6
|
const highlightSupport = {
|
|
12
7
|
|
|
8
|
+
// Used to highlight arbitrary text in an editable. All occurrences
|
|
9
|
+
// will be highlighted.
|
|
13
10
|
highlightText (editableHost, text, highlightId, type) {
|
|
14
11
|
if (this.hasHighlight(editableHost, highlightId)) return
|
|
15
12
|
const blockText = highlightText.extractText(editableHost)
|
|
@@ -17,8 +14,7 @@ const highlightSupport = {
|
|
|
17
14
|
const marker = `<span class="highlight-${type}"></span>`
|
|
18
15
|
const markerNode = highlightSupport.createMarkerNode(marker, type, this.win)
|
|
19
16
|
|
|
20
|
-
const
|
|
21
|
-
const matches = textSearch.findMatches(blockText, [text])
|
|
17
|
+
const matches = searchText(blockText, text, markerNode)
|
|
22
18
|
|
|
23
19
|
if (matches && matches.length) {
|
|
24
20
|
if (highlightId) matches[0].id = highlightId
|
|
@@ -27,42 +23,39 @@ const highlightSupport = {
|
|
|
27
23
|
}
|
|
28
24
|
},
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
// This function was changed to track matches when text is added to the start
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
26
|
+
// Used to highlight comments.
|
|
27
|
+
// This function was changed to track matches when text is added to the start
|
|
28
|
+
// of a component, but multiple white spaces break it in a strict sense
|
|
29
|
+
// The function works in the editor and in browsers, but tests with
|
|
30
|
+
// multiple white spaces will fail.
|
|
31
|
+
// Browsers change the white spaces to   and the function works,
|
|
32
|
+
// and the tests in highlight.spec.js have been updated to represent this.
|
|
35
33
|
highlightRange (editableHost, text, highlightId, startIndex, endIndex, dispatcher, type = 'comment') {
|
|
36
34
|
if (this.hasHighlight(editableHost, highlightId)) {
|
|
37
35
|
this.removeHighlight(editableHost, highlightId)
|
|
38
36
|
}
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
const blockText = highlightText.extractText(editableHost)
|
|
43
|
-
if (blockText === '') return -1 // the text was deleted so we can't highlight it
|
|
44
|
-
const matchesArray = textSearch.findMatches(blockText, [text])
|
|
45
|
-
const {actualStartIndex, actualEndIndex} = this.getIndex(matchesArray, startIndex, endIndex)
|
|
46
|
-
const range = rangy.createRange()
|
|
47
|
-
range.selectCharacters(editableHost, actualStartIndex, actualEndIndex)
|
|
48
|
-
|
|
49
|
-
if (!isInHost(range.commonAncestorContainer, editableHost)) {
|
|
50
|
-
return -1
|
|
51
|
-
}
|
|
37
|
+
|
|
38
|
+
const blockText = highlightText.extractText(editableHost, false)
|
|
39
|
+
if (blockText === '') return -1 // the text was deleted so we can't highlight anything
|
|
52
40
|
|
|
53
41
|
const marker = highlightSupport.createMarkerNode(
|
|
54
|
-
`<span class="highlight-${type}"
|
|
42
|
+
`<span class="highlight-${type}"></span>`,
|
|
55
43
|
type,
|
|
56
44
|
this.win
|
|
57
45
|
)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
46
|
+
|
|
47
|
+
const actualStartIndex = startIndex
|
|
48
|
+
const actualEndIndex = endIndex
|
|
49
|
+
|
|
50
|
+
highlightText.highlightMatches(editableHost, [{
|
|
51
|
+
startIndex: actualStartIndex,
|
|
52
|
+
endIndex: actualEndIndex,
|
|
53
|
+
id: highlightId,
|
|
54
|
+
marker
|
|
55
|
+
}], false)
|
|
56
|
+
|
|
57
|
+
if (dispatcher) dispatcher.notify('change', editableHost)
|
|
58
|
+
|
|
66
59
|
return actualStartIndex
|
|
67
60
|
},
|
|
68
61
|
|
|
@@ -80,10 +73,12 @@ const highlightSupport = {
|
|
|
80
73
|
const elems = editableHost.querySelectorAll(`[data-word-id="${highlightId}"]`)
|
|
81
74
|
for (const elem of elems) {
|
|
82
75
|
content.unwrap(elem)
|
|
83
|
-
// in Chrome browsers the unwrap method leaves the host node split into 2 (lastChild !== firstChild)
|
|
84
|
-
editableHost.normalize()
|
|
85
|
-
if (dispatcher) dispatcher.notify('change', editableHost)
|
|
86
76
|
}
|
|
77
|
+
|
|
78
|
+
// remove empty text nodes, combine adjacent text nodes
|
|
79
|
+
editableHost.normalize()
|
|
80
|
+
|
|
81
|
+
if (dispatcher) dispatcher.notify('change', editableHost)
|
|
87
82
|
},
|
|
88
83
|
|
|
89
84
|
hasHighlight (editableHost, highlightId) {
|
|
@@ -103,7 +98,6 @@ const highlightSupport = {
|
|
|
103
98
|
if (!groups[highlightId]) {
|
|
104
99
|
groups[highlightId] = editableHost.querySelectorAll(`[data-word-id="${highlightId}"]`)
|
|
105
100
|
}
|
|
106
|
-
|
|
107
101
|
}
|
|
108
102
|
|
|
109
103
|
const res = {}
|
|
@@ -116,7 +110,7 @@ const highlightSupport = {
|
|
|
116
110
|
},
|
|
117
111
|
|
|
118
112
|
extractMarkerNodePosition (editableHost, markers) {
|
|
119
|
-
const range =
|
|
113
|
+
const range = createRange()
|
|
120
114
|
if (markers.length > 1) {
|
|
121
115
|
range.setStartBefore(markers[0])
|
|
122
116
|
range.setEndAfter(markers[markers.length - 1])
|
|
@@ -124,12 +118,12 @@ const highlightSupport = {
|
|
|
124
118
|
range.selectNode(markers[0])
|
|
125
119
|
}
|
|
126
120
|
|
|
127
|
-
const textRange =
|
|
121
|
+
const textRange = toCharacterRange(range, editableHost)
|
|
128
122
|
return {
|
|
129
123
|
start: textRange.start,
|
|
130
124
|
end: textRange.end,
|
|
131
|
-
text:
|
|
132
|
-
nativeRange: range
|
|
125
|
+
text: textRange.text, // browser range result (does whitespace normalization)
|
|
126
|
+
nativeRange: range
|
|
133
127
|
}
|
|
134
128
|
},
|
|
135
129
|
|
|
@@ -147,20 +141,7 @@ const highlightSupport = {
|
|
|
147
141
|
marker.setAttribute('data-editable', 'ui-unwrap')
|
|
148
142
|
marker.setAttribute('data-highlight', highlightType)
|
|
149
143
|
return marker
|
|
150
|
-
},
|
|
151
|
-
|
|
152
|
-
// This function checks to see if text has been added to the component before the comment
|
|
153
|
-
// If it has, the start index is updated, otherwise it remains the same
|
|
154
|
-
getIndex (matchesArray, startIndex, endIndex) {
|
|
155
|
-
const newStartIndex = matchesArray.find((match) => {
|
|
156
|
-
return match.startIndex >= startIndex // checks if the startIndex has increased
|
|
157
|
-
})
|
|
158
|
-
const actualStartIndex = newStartIndex ? newStartIndex.startIndex : startIndex
|
|
159
|
-
const actualEndIndex = actualStartIndex ? (actualStartIndex + (endIndex - startIndex)) : endIndex
|
|
160
|
-
|
|
161
|
-
return {actualStartIndex, actualEndIndex}
|
|
162
144
|
}
|
|
163
|
-
|
|
164
145
|
}
|
|
165
146
|
|
|
166
147
|
export default highlightSupport
|