@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/dist/editable.js +1 -1
- package/dist/editable.js.map +1 -1
- package/package.json +5 -6
- package/src/content.js +92 -20
- package/src/core.js +10 -21
- package/src/cursor.js +20 -14
- package/src/dispatcher.js +8 -8
- 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/src/highlight-text.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import rangy from 'rangy'
|
|
2
|
-
|
|
3
1
|
import NodeIterator from './node-iterator'
|
|
4
2
|
import * as nodeType from './node-type'
|
|
3
|
+
import {createRange} from './util/dom'
|
|
5
4
|
|
|
6
5
|
export default {
|
|
7
6
|
|
|
8
7
|
// Get the text from an editable block with a NodeIterator.
|
|
9
8
|
// This must work the same as when later iterating over the text
|
|
10
9
|
// in highlightMatches().
|
|
11
|
-
extractText (element) {
|
|
10
|
+
extractText (element, convertBRs = true) {
|
|
12
11
|
let text = ''
|
|
13
|
-
getText(element, (part) => { text += part })
|
|
12
|
+
getText(element, convertBRs, (part) => { text += part })
|
|
14
13
|
return text
|
|
15
14
|
},
|
|
16
15
|
|
|
@@ -21,12 +20,21 @@ export default {
|
|
|
21
20
|
// @params
|
|
22
21
|
// - matches
|
|
23
22
|
// Array of positions in the string to highlight:
|
|
24
|
-
// e.g [{
|
|
25
|
-
|
|
23
|
+
// e.g [{
|
|
24
|
+
// startIndex: 0,
|
|
25
|
+
// endIndex: 1,
|
|
26
|
+
// match: 'The', // not used, only the indexes are used for highlighting)
|
|
27
|
+
// marker: DOMNode, // A clone of this element will be inserted
|
|
28
|
+
// id: 'a7382', // used in word-id attribute
|
|
29
|
+
// title: 'The World' // used in title attribute (optional)
|
|
30
|
+
// }]
|
|
31
|
+
highlightMatches (element, matches, countBRs = true) {
|
|
26
32
|
if (!matches || matches.length === 0) {
|
|
27
33
|
return
|
|
28
34
|
}
|
|
29
35
|
|
|
36
|
+
element.normalize() // mend text nodes
|
|
37
|
+
|
|
30
38
|
const iterator = new NodeIterator(element)
|
|
31
39
|
let currentMatchIndex = 0
|
|
32
40
|
let totalOffset = 0
|
|
@@ -39,7 +47,7 @@ export default {
|
|
|
39
47
|
// Account for <br> elements
|
|
40
48
|
if (next.nodeType === nodeType.textNode && next.data !== '') {
|
|
41
49
|
textNode = next
|
|
42
|
-
} else if (next.nodeType === nodeType.elementNode && next.nodeName === 'BR') {
|
|
50
|
+
} else if (countBRs && next.nodeType === nodeType.elementNode && next.nodeName === 'BR') {
|
|
43
51
|
totalOffset += 1
|
|
44
52
|
continue
|
|
45
53
|
} else {
|
|
@@ -109,7 +117,7 @@ export default {
|
|
|
109
117
|
},
|
|
110
118
|
|
|
111
119
|
wrapPortion (portion, stencilElement, title) {
|
|
112
|
-
const range =
|
|
120
|
+
const range = createRange()
|
|
113
121
|
range.setStart(portion.element, portion.offset)
|
|
114
122
|
range.setEnd(portion.element, portion.offset + portion.length)
|
|
115
123
|
const node = stencilElement.cloneNode(true)
|
|
@@ -136,14 +144,14 @@ export default {
|
|
|
136
144
|
// with data-editable="remove"
|
|
137
145
|
// - It returns a \n for <br> elements
|
|
138
146
|
// (The only block level element allowed inside of editables)
|
|
139
|
-
function getText (element,
|
|
147
|
+
function getText (element, convertBRs, func) {
|
|
140
148
|
const iterator = new NodeIterator(element)
|
|
141
149
|
let next
|
|
142
150
|
while ((next = iterator.getNext())) {
|
|
143
151
|
if (next.nodeType === nodeType.textNode && next.data !== '') {
|
|
144
|
-
|
|
145
|
-
} else if (next.nodeType === nodeType.elementNode && next.nodeName === 'BR') {
|
|
146
|
-
|
|
152
|
+
func(next.data)
|
|
153
|
+
} else if (convertBRs && next.nodeType === nodeType.elementNode && next.nodeName === 'BR') {
|
|
154
|
+
func('\n')
|
|
147
155
|
}
|
|
148
156
|
}
|
|
149
157
|
}
|
package/src/keyboard.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {createRange, containsRange} from './util/dom'
|
|
3
2
|
import {contenteditableSpanBug} from './feature-detection'
|
|
4
3
|
import * as nodeType from './node-type'
|
|
5
4
|
import eventable from './eventable'
|
|
@@ -118,10 +117,10 @@ export default class Keyboard {
|
|
|
118
117
|
//
|
|
119
118
|
// Manually remove the element that would be removed anyway before inserting
|
|
120
119
|
// the new letter.
|
|
121
|
-
const
|
|
122
|
-
if (!
|
|
120
|
+
const rangeContainer = this.selectionWatcher.getFreshRange()
|
|
121
|
+
if (!rangeContainer.isSelection) return
|
|
123
122
|
|
|
124
|
-
const nodeToRemove = Keyboard.getNodeToRemove(
|
|
123
|
+
const nodeToRemove = Keyboard.getNodeToRemove(rangeContainer.range, target)
|
|
125
124
|
if (nodeToRemove) nodeToRemove.remove()
|
|
126
125
|
}
|
|
127
126
|
|
|
@@ -144,10 +143,10 @@ export default class Keyboard {
|
|
|
144
143
|
|
|
145
144
|
// We get a range that contains everything within the sartNodeElement to test
|
|
146
145
|
// if the selectionRange is within the startNode, we have nothing to do.
|
|
147
|
-
const startNodeRange =
|
|
146
|
+
const startNodeRange = createRange()
|
|
148
147
|
startNodeRange.setStartBefore(startNodeElement.firstChild)
|
|
149
148
|
startNodeRange.setEndAfter(startNodeElement.lastChild)
|
|
150
|
-
if (
|
|
149
|
+
if (containsRange(startNodeRange, selectionRange)) return
|
|
151
150
|
|
|
152
151
|
// If the selectionRange.startContainer was a textNode, we have to make sure
|
|
153
152
|
// that its parent's content starts with this node. Content is either a
|
|
@@ -4,12 +4,18 @@ import * as content from './content'
|
|
|
4
4
|
import highlightText from './highlight-text'
|
|
5
5
|
import SpellcheckService from './plugins/highlighting/spellcheck-service'
|
|
6
6
|
import WhitespaceHighlighting from './plugins/highlighting/whitespace-highlighting'
|
|
7
|
-
import
|
|
7
|
+
import {searchAllWords} from './plugins/highlighting/text-search'
|
|
8
8
|
import MatchCollection from './plugins/highlighting/match-collection'
|
|
9
9
|
import highlightSupport from './highlight-support'
|
|
10
10
|
import {domArray, domSelector} from './util/dom'
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
// Spellcheck and Whitespace Highlighting
|
|
13
|
+
// --------------------------------------
|
|
14
|
+
//
|
|
15
|
+
// This instance monitors an editable block for changes and
|
|
16
|
+
// updates highlights accordingly. It also calls the spellcheck
|
|
17
|
+
// service after the content has changed.
|
|
18
|
+
export default class MonitoredHighlighting {
|
|
13
19
|
|
|
14
20
|
constructor (editable, configuration, spellcheckConfig) {
|
|
15
21
|
this.editable = editable
|
|
@@ -41,13 +47,12 @@ export default class Highlighting {
|
|
|
41
47
|
const spellcheckService = this.config.spellcheck.spellcheckService
|
|
42
48
|
const spellcheckMarker = this.config.spellcheck.marker
|
|
43
49
|
const whitespaceMarker = this.config.whitespace.marker
|
|
44
|
-
const spellcheckMarkerNode = highlightSupport
|
|
45
|
-
.createMarkerNode(spellcheckMarker, 'spellcheck', this.win)
|
|
46
50
|
const whitespaceMarkerNode = highlightSupport
|
|
47
|
-
.createMarkerNode(whitespaceMarker, '
|
|
51
|
+
.createMarkerNode(whitespaceMarker, 'whitespace', this.win)
|
|
52
|
+
this.spellcheckMarkerNode = highlightSupport
|
|
53
|
+
.createMarkerNode(spellcheckMarker, 'spellcheck', this.win)
|
|
48
54
|
|
|
49
55
|
this.spellcheckService = new SpellcheckService(spellcheckService)
|
|
50
|
-
this.spellcheck = new WordHighlighting(spellcheckMarkerNode)
|
|
51
56
|
this.whitespace = new WhitespaceHighlighting(whitespaceMarkerNode)
|
|
52
57
|
|
|
53
58
|
this.setupListeners()
|
|
@@ -116,22 +121,22 @@ export default class Highlighting {
|
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
highlight (editableHost) {
|
|
119
|
-
|
|
124
|
+
const textBefore = highlightText.extractText(editableHost)
|
|
120
125
|
|
|
121
126
|
// getSpellcheck
|
|
122
|
-
this.spellcheckService.check(
|
|
123
|
-
if (err) { return }
|
|
127
|
+
this.spellcheckService.check(textBefore, (err, misspelledWords) => {
|
|
128
|
+
if (err || !editableHost.isConnected) { return } // return in case the host was removed from the dom
|
|
124
129
|
|
|
125
130
|
// refresh the text
|
|
126
|
-
text = highlightText.extractText(editableHost)
|
|
131
|
+
const text = highlightText.extractText(editableHost)
|
|
127
132
|
|
|
128
133
|
const matchCollection = new MatchCollection()
|
|
129
134
|
|
|
130
|
-
let matches =
|
|
131
|
-
matchCollection.addMatches(
|
|
135
|
+
let matches = searchAllWords(text, misspelledWords, this.spellcheckMarkerNode)
|
|
136
|
+
matchCollection.addMatches(matches)
|
|
132
137
|
|
|
133
138
|
matches = this.whitespace.findMatches(text)
|
|
134
|
-
matchCollection.addMatches(
|
|
139
|
+
matchCollection.addMatches(matches)
|
|
135
140
|
|
|
136
141
|
this.safeHighlightMatches(editableHost, matchCollection.matches)
|
|
137
142
|
})
|
|
@@ -166,7 +171,7 @@ export default class Highlighting {
|
|
|
166
171
|
|
|
167
172
|
removeHighlights (editableHost) {
|
|
168
173
|
editableHost = domSelector(editableHost, this.win.document)
|
|
169
|
-
for (const elem of domArray('[data-highlight="spellcheck"]', editableHost)) {
|
|
174
|
+
for (const elem of domArray('[data-highlight="spellcheck"], [data-highlight="whitespace"]', editableHost)) {
|
|
170
175
|
content.unwrap(elem)
|
|
171
176
|
}
|
|
172
177
|
}
|
|
@@ -184,7 +189,7 @@ export default class Highlighting {
|
|
|
184
189
|
do {
|
|
185
190
|
if (elementAtCursor === editableHost) return
|
|
186
191
|
const highlightType = elementAtCursor.getAttribute('data-highlight')
|
|
187
|
-
if (highlightType === 'spellcheck') {
|
|
192
|
+
if (highlightType === 'spellcheck' || highlightType === 'whitespace') {
|
|
188
193
|
wordId = elementAtCursor.getAttribute('data-word-id')
|
|
189
194
|
break
|
|
190
195
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export function searchText (text, searchTerm, marker) {
|
|
2
|
+
const matchMode = 'text'
|
|
3
|
+
return findMatches(text, [searchTerm], matchMode, marker)
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function searchWord (text, word, marker) {
|
|
7
|
+
const matchMode = 'word'
|
|
8
|
+
return findMatches(text, [word], matchMode, marker)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function searchAllWords (text, words, marker) {
|
|
12
|
+
const matchMode = 'word'
|
|
13
|
+
return findMatches(text, words, matchMode, marker)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function findMatches (text, searchTexts, matchMode, marker) {
|
|
17
|
+
if (!text || text === '') return []
|
|
18
|
+
if (marker && !isElement(marker)) return []
|
|
19
|
+
if (!searchTexts?.length) return []
|
|
20
|
+
|
|
21
|
+
const createRegex = matchMode === 'word'
|
|
22
|
+
? createWordRegex
|
|
23
|
+
: createHighlightRegex
|
|
24
|
+
|
|
25
|
+
const regex = createRegex(searchTexts)
|
|
26
|
+
const matches = [...text.matchAll(regex)]
|
|
27
|
+
|
|
28
|
+
return matches.map((match) => {
|
|
29
|
+
let startIndex
|
|
30
|
+
let matchedText
|
|
31
|
+
if (matchMode === 'word') {
|
|
32
|
+
startIndex = match.index + match[1].length
|
|
33
|
+
matchedText = match[2]
|
|
34
|
+
} else {
|
|
35
|
+
startIndex = match.index
|
|
36
|
+
matchedText = match[0]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
startIndex,
|
|
41
|
+
endIndex: startIndex + matchedText.length,
|
|
42
|
+
match: matchedText,
|
|
43
|
+
marker
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isElement (obj) {
|
|
49
|
+
try {
|
|
50
|
+
if (!obj) return false
|
|
51
|
+
return obj instanceof obj.ownerDocument?.defaultView.HTMLElement
|
|
52
|
+
} catch (e) {
|
|
53
|
+
// Browsers not supporting W3 DOM2 don't have HTMLElement and
|
|
54
|
+
// an exception is thrown and we end up here. Testing some
|
|
55
|
+
// properties that all elements have (works on IE7)
|
|
56
|
+
return (typeof obj === 'object') &&
|
|
57
|
+
(obj.nodeType === 1) && (typeof obj.style === 'object') &&
|
|
58
|
+
(typeof obj.ownerDocument === 'object')
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createHighlightRegex (words = []) {
|
|
63
|
+
const escapedWords = words.map((word) => escapeRegEx(word))
|
|
64
|
+
|
|
65
|
+
const regex = `(${escapedWords.join('|')})`
|
|
66
|
+
return new RegExp(regex, 'g')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Regex to find whole words within a string
|
|
70
|
+
//
|
|
71
|
+
// Unicode character blocks for letters.
|
|
72
|
+
// See: http://jrgraphix.net/research/unicode_blocks.php
|
|
73
|
+
//
|
|
74
|
+
// \\u0041-\\u005A A-Z (Basic Latin)
|
|
75
|
+
// \\u0061-\\u007A a-z (Basic Latin)
|
|
76
|
+
// \\u0030-\\u0039 0-9 (Basic Latin)
|
|
77
|
+
// \\u00AA ª (Latin-1 Supplement)
|
|
78
|
+
// \\u00B5 µ (Latin-1 Supplement)
|
|
79
|
+
// \\u00BA º (Latin-1 Supplement)
|
|
80
|
+
// \\u00C0-\\u00D6 À-Ö (Latin-1 Supplement)
|
|
81
|
+
// \\u00D8-\\u00F6 Ø-ö (Latin-1 Supplement)
|
|
82
|
+
// \\u00F8-\\u00FF ø-ÿ (Latin-1 Supplement)
|
|
83
|
+
// \\u0100-\\u017F Ā-ſ (Latin Extended-A)
|
|
84
|
+
// \\u0180-\\u024F ƀ-ɏ (Latin Extended-B)
|
|
85
|
+
const letterChars = '\\u0041-\\u005A\\u0061-\\u007A\\u0030-\\u0039\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u00FF\\u0100-\\u017F\\u0180-\\u024F'
|
|
86
|
+
|
|
87
|
+
function createWordRegex (words = []) {
|
|
88
|
+
const escapedWords = words.map((word) => escapeRegEx(word))
|
|
89
|
+
|
|
90
|
+
// (notLetter|^)(words)(?=notLetter|$)
|
|
91
|
+
const regex = `([^${letterChars}]|^)` +
|
|
92
|
+
`(${escapedWords.join('|')})` +
|
|
93
|
+
`(?=[^${letterChars}]|$)`
|
|
94
|
+
|
|
95
|
+
return new RegExp(regex, 'g')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function escapeRegEx (s) {
|
|
99
|
+
return String(s).replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1')
|
|
100
|
+
}
|
|
@@ -23,6 +23,7 @@ const characters = {
|
|
|
23
23
|
// a paragraph and the highlighting prevents browsers from converting
|
|
24
24
|
// the no-break space back to a normal space when the user keeps typing.
|
|
25
25
|
const specialWhitespaceChars = '\\u2000-\\u200A\\u202F\\u205F\\u3000'
|
|
26
|
+
const specialWhitespaceCharsRegex = new RegExp(`[${specialWhitespaceChars}]`, 'g')
|
|
26
27
|
|
|
27
28
|
export default class WhitespaceHighlighting {
|
|
28
29
|
|
|
@@ -33,13 +34,7 @@ export default class WhitespaceHighlighting {
|
|
|
33
34
|
findMatches (text) {
|
|
34
35
|
if (!text) return
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
regex = new RegExp(regex, 'g')
|
|
38
|
-
|
|
39
|
-
const matches = []
|
|
40
|
-
let match
|
|
41
|
-
while ((match = regex.exec(text))) matches.push(match)
|
|
42
|
-
|
|
37
|
+
const matches = [...text.matchAll(specialWhitespaceCharsRegex)]
|
|
43
38
|
return matches.map((entry) => this.prepareMatch(entry))
|
|
44
39
|
}
|
|
45
40
|
|
package/src/range-container.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Cursor from './cursor'
|
|
2
2
|
import Selection from './selection'
|
|
3
|
+
import {rangesAreEqual} from './util/dom'
|
|
3
4
|
|
|
4
5
|
/** RangeContainer
|
|
5
6
|
*
|
|
@@ -10,13 +11,13 @@ import Selection from './selection'
|
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
export default class RangeContainer {
|
|
13
|
-
constructor (editableHost,
|
|
14
|
+
constructor (editableHost, range) {
|
|
14
15
|
this.host = editableHost && editableHost.jquery
|
|
15
16
|
? editableHost[0]
|
|
16
17
|
: editableHost
|
|
17
|
-
this.range =
|
|
18
|
-
this.isAnythingSelected = (
|
|
19
|
-
this.isCursor = (this.isAnythingSelected &&
|
|
18
|
+
this.range = range
|
|
19
|
+
this.isAnythingSelected = (range !== undefined)
|
|
20
|
+
this.isCursor = (this.isAnythingSelected && range.collapsed)
|
|
20
21
|
this.isSelection = (this.isAnythingSelected && !this.isCursor)
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -36,7 +37,7 @@ export default class RangeContainer {
|
|
|
36
37
|
isDifferentFrom (otherRangeContainer = new RangeContainer()) {
|
|
37
38
|
const self = this.range
|
|
38
39
|
const other = otherRangeContainer.range
|
|
39
|
-
if (self && other) return !self
|
|
40
|
+
if (self && other) return !rangesAreEqual(self, other)
|
|
40
41
|
if (!self && !other) return false
|
|
41
42
|
return true
|
|
42
43
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import rangy from 'rangy'
|
|
2
1
|
import error from './util/error'
|
|
3
2
|
import * as nodeType from './node-type'
|
|
3
|
+
import {createRange, normalizeBoundaries} from './util/dom'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Inspired by the Selection save and restore module for Rangy by Tim Down
|
|
@@ -11,35 +11,19 @@ let boundaryMarkerId = 0
|
|
|
11
11
|
// (U+FEFF) zero width no-break space
|
|
12
12
|
const markerTextChar = '\ufeff'
|
|
13
13
|
|
|
14
|
-
function
|
|
15
|
-
for (let i = 0; i < parent.children.length; i++) {
|
|
16
|
-
if (parent.children[i] === possibleChild) return true
|
|
17
|
-
}
|
|
18
|
-
return false
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function startContainerIsChild (range) {
|
|
22
|
-
const parent = range.commonAncestorContainer
|
|
23
|
-
if (parent.nodeType === 3) return false // if we are on the text node it can't be a parent
|
|
24
|
-
const possibleChild = range.startContainer.parentElement
|
|
25
|
-
return isChildOf(parent, possibleChild)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function endContainerIsChild (range) {
|
|
14
|
+
function isSecondChildOfCommonAncestor (range, rangeContainer) {
|
|
29
15
|
const parent = range.commonAncestorContainer
|
|
30
16
|
if (parent.nodeType === 3) return false // if we are on the text node it can't be a parent
|
|
31
|
-
const possibleChild =
|
|
32
|
-
return
|
|
17
|
+
const possibleChild = rangeContainer.parentElement
|
|
18
|
+
return possibleChild?.parentElement === parent
|
|
33
19
|
}
|
|
34
20
|
|
|
35
21
|
export function insertRangeBoundaryMarker (range, atStart) {
|
|
36
22
|
const container = range.commonAncestorContainer
|
|
37
|
-
const directlyBeforeFormatTag = startContainerIsChild(range) && atStart
|
|
38
|
-
const directlyAfterFormatTag = endContainerIsChild(range) && !atStart
|
|
39
23
|
|
|
40
24
|
// If ownerDocument is null the commonAncestorContainer is window.document
|
|
41
|
-
if (container.ownerDocument
|
|
42
|
-
error('Cannot save range: range is
|
|
25
|
+
if (!container.ownerDocument) {
|
|
26
|
+
error('Cannot save range: range is empty')
|
|
43
27
|
}
|
|
44
28
|
|
|
45
29
|
// Create the marker element containing a single
|
|
@@ -52,14 +36,21 @@ export function insertRangeBoundaryMarker (range, atStart) {
|
|
|
52
36
|
markerEl.style.display = 'none'
|
|
53
37
|
markerEl.appendChild(doc.createTextNode(markerTextChar))
|
|
54
38
|
|
|
55
|
-
//
|
|
56
|
-
//
|
|
39
|
+
// This logic can expand the selection by inserting the marker before the element
|
|
40
|
+
// of the start or end container. In some cases this prevents breaking up existing tags.
|
|
41
|
+
// The solution is not perfect, but has been here a while.
|
|
42
|
+
// And it is somewhat inconsistent as it check if the container is a second grade child
|
|
43
|
+
// of the common ancestor.
|
|
44
|
+
// It can help to prevent the nuking of e.g. comments when formatting like bold
|
|
45
|
+
// is applied.
|
|
46
|
+
const directlyBeforeFormatTag = atStart && isSecondChildOfCommonAncestor(range, range.startContainer)
|
|
47
|
+
const directlyAfterFormatTag = !atStart && isSecondChildOfCommonAncestor(range, range.endContainer)
|
|
57
48
|
if (directlyBeforeFormatTag) {
|
|
58
|
-
|
|
49
|
+
const startParentElem = range.startContainer.parentElement
|
|
50
|
+
startParentElem.parentElement.insertBefore(markerEl, startParentElem)
|
|
59
51
|
} else if (directlyAfterFormatTag) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
range.endContainer.parentElement.nextSibling)
|
|
52
|
+
const endParentElem = range.endContainer.parentElement
|
|
53
|
+
endParentElem.parentElement.insertBefore(markerEl, endParentElem.nextSibling)
|
|
63
54
|
} else {
|
|
64
55
|
// Clone the Range and collapse to the appropriate boundary point
|
|
65
56
|
const boundaryRange = range.cloneRange()
|
|
@@ -100,7 +91,8 @@ export function save (range) {
|
|
|
100
91
|
|
|
101
92
|
// Adjust each range's boundaries to lie between its markers
|
|
102
93
|
if (range.collapsed) {
|
|
103
|
-
range.
|
|
94
|
+
range.setStartBefore(endEl)
|
|
95
|
+
range.collapse(true)
|
|
104
96
|
} else {
|
|
105
97
|
range.setEndBefore(endEl)
|
|
106
98
|
range.setStartAfter(startEl)
|
|
@@ -112,7 +104,7 @@ export function save (range) {
|
|
|
112
104
|
export function restore (host, rangeInfo) {
|
|
113
105
|
if (rangeInfo.restored) return
|
|
114
106
|
|
|
115
|
-
const range =
|
|
107
|
+
const range = createRange()
|
|
116
108
|
if (rangeInfo.collapsed) {
|
|
117
109
|
const markerEl = getMarker(host, rangeInfo.markerId)
|
|
118
110
|
if (markerEl) {
|
|
@@ -122,9 +114,10 @@ export function restore (host, rangeInfo) {
|
|
|
122
114
|
// Workaround for rangy issue 17
|
|
123
115
|
if (previousNode && previousNode.nodeType === nodeType.textNode) {
|
|
124
116
|
markerEl.remove()
|
|
125
|
-
range.
|
|
117
|
+
range.setStart(previousNode, previousNode.length)
|
|
126
118
|
} else {
|
|
127
|
-
range.
|
|
119
|
+
range.setStartBefore(markerEl)
|
|
120
|
+
range.collapse(true)
|
|
128
121
|
markerEl.remove()
|
|
129
122
|
}
|
|
130
123
|
} else {
|
|
@@ -135,7 +128,7 @@ export function restore (host, rangeInfo) {
|
|
|
135
128
|
setRangeBoundary(host, range, rangeInfo.endMarkerId, false)
|
|
136
129
|
}
|
|
137
130
|
|
|
138
|
-
|
|
131
|
+
normalizeBoundaries(range)
|
|
139
132
|
return range
|
|
140
133
|
}
|
|
141
134
|
|
package/src/selection-watcher.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import rangy from 'rangy'
|
|
2
|
-
|
|
3
1
|
import * as parser from './parser'
|
|
4
2
|
import RangeContainer from './range-container'
|
|
5
3
|
import Cursor from './cursor'
|
|
6
4
|
import Selection from './selection'
|
|
5
|
+
import {getSelection} from './util/dom'
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* The SelectionWatcher module watches for selection changes inside
|
|
@@ -17,19 +16,19 @@ export default class SelectionWatcher {
|
|
|
17
16
|
constructor (dispatcher, win) {
|
|
18
17
|
this.dispatcher = dispatcher
|
|
19
18
|
this.win = win || window
|
|
20
|
-
this.
|
|
19
|
+
this.selection = undefined
|
|
21
20
|
this.currentSelection = undefined
|
|
22
21
|
this.currentRange = undefined
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
/**
|
|
26
|
-
* Updates the internal selection pointer to the current
|
|
25
|
+
* Updates the internal selection pointer to the current selection.
|
|
27
26
|
* Returns true if no exception occurred.
|
|
28
27
|
*/
|
|
29
28
|
syncSelection () {
|
|
30
|
-
//
|
|
29
|
+
// Note: the try catch was introduced because of rangy exceptions with nativeSelections
|
|
31
30
|
try {
|
|
32
|
-
this.
|
|
31
|
+
this.selection = getSelection(this.win)
|
|
33
32
|
} catch (err) {
|
|
34
33
|
return false
|
|
35
34
|
}
|
|
@@ -47,8 +46,8 @@ export default class SelectionWatcher {
|
|
|
47
46
|
// rangeCount is 0 or 1 in all browsers except firefox
|
|
48
47
|
// firefox can work with multiple ranges
|
|
49
48
|
// (on a mac hold down the command key to select multiple ranges)
|
|
50
|
-
if (this.
|
|
51
|
-
const range = this.
|
|
49
|
+
if (this.selection.rangeCount && successfulSync) {
|
|
50
|
+
const range = this.selection.getRangeAt(0)
|
|
52
51
|
const hostNode = parser.getHost(range.commonAncestorContainer)
|
|
53
52
|
if (hostNode) return new RangeContainer(hostNode, range)
|
|
54
53
|
}
|
|
@@ -73,11 +72,11 @@ export default class SelectionWatcher {
|
|
|
73
72
|
* there is neither a selection or cursor.
|
|
74
73
|
*/
|
|
75
74
|
getFreshSelection () {
|
|
76
|
-
const
|
|
75
|
+
const rangeContainer = this.getRangeContainer()
|
|
77
76
|
|
|
78
|
-
return
|
|
79
|
-
?
|
|
80
|
-
:
|
|
77
|
+
return rangeContainer.isCursor
|
|
78
|
+
? rangeContainer.getCursor(this.win)
|
|
79
|
+
: rangeContainer.getSelection(this.win)
|
|
81
80
|
}
|
|
82
81
|
|
|
83
82
|
/**
|
|
@@ -94,15 +93,15 @@ export default class SelectionWatcher {
|
|
|
94
93
|
}
|
|
95
94
|
|
|
96
95
|
forceCursor () {
|
|
97
|
-
const
|
|
98
|
-
return
|
|
96
|
+
const rangeContainer = this.getRangeContainer()
|
|
97
|
+
return rangeContainer.forceCursor()
|
|
99
98
|
}
|
|
100
99
|
|
|
101
100
|
selectionChanged () {
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
101
|
+
const newRangeContainer = this.getRangeContainer()
|
|
102
|
+
if (newRangeContainer.isDifferentFrom(this.currentRange)) {
|
|
104
103
|
const lastSelection = this.currentSelection
|
|
105
|
-
this.currentRange =
|
|
104
|
+
this.currentRange = newRangeContainer
|
|
106
105
|
|
|
107
106
|
// empty selection or cursor
|
|
108
107
|
if (lastSelection) {
|