@livingdocs/editable.js 3.1.7 → 3.2.1
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 +13 -10
- package/src/block.js +6 -1
- package/src/clipboard.js +29 -21
- package/src/config.js +3 -0
- package/src/core.js +19 -11
- package/src/selection.js +28 -3
- package/src/util/dom.js +88 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livingdocs/editable.js",
|
|
3
3
|
"description": "Friendly contenteditable API",
|
|
4
|
-
"version": "3.1
|
|
4
|
+
"version": "3.2.1",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@babel/runtime": "^7.16.7",
|
|
7
7
|
"bowser": "2.11.0",
|
|
@@ -21,8 +21,6 @@
|
|
|
21
21
|
"css-loader": "^6.5.1",
|
|
22
22
|
"eslint": "^8.8.0",
|
|
23
23
|
"eslint-plugin-react": "^7.28.0",
|
|
24
|
-
"font-awesome": "^4.7.0",
|
|
25
|
-
"gh-pages": "^5.0.0",
|
|
26
24
|
"karma": "^6.3.13",
|
|
27
25
|
"karma-chrome-launcher": "^3.1.0",
|
|
28
26
|
"karma-coverage": "^2.1.0",
|
|
@@ -37,7 +35,7 @@
|
|
|
37
35
|
"react": "^18.0.0",
|
|
38
36
|
"react-dom": "^18.0.0",
|
|
39
37
|
"react-transition-group": "^4.4.2",
|
|
40
|
-
"rimraf": "^
|
|
38
|
+
"rimraf": "^5.0.0",
|
|
41
39
|
"sinon": "^15.0.0",
|
|
42
40
|
"style-loader": "^3.3.1",
|
|
43
41
|
"url-loader": "^4.1.1",
|
|
@@ -46,7 +44,8 @@
|
|
|
46
44
|
"webpack-dev-server": "^4.7.3"
|
|
47
45
|
},
|
|
48
46
|
"engines": {
|
|
49
|
-
"node": ">=12"
|
|
47
|
+
"node": ">=12",
|
|
48
|
+
"npm": ">=6"
|
|
50
49
|
},
|
|
51
50
|
"files": [
|
|
52
51
|
"src",
|
|
@@ -76,18 +75,22 @@
|
|
|
76
75
|
"test:watch": "npm run test:karma -s -- --single-run false",
|
|
77
76
|
"lint": "eslint --ignore-path .gitignore ."
|
|
78
77
|
},
|
|
78
|
+
"publishConfig": {
|
|
79
|
+
"tag": "3.x"
|
|
80
|
+
},
|
|
79
81
|
"release": {
|
|
80
|
-
"extends": "@livingdocs/semantic-release-presets/npm-github-verify"
|
|
82
|
+
"extends": "@livingdocs/semantic-release-presets/npm-github-verify",
|
|
83
|
+
"branch": "3.x"
|
|
81
84
|
},
|
|
82
85
|
"ci": {
|
|
83
86
|
"isCi": true,
|
|
84
87
|
"name": "Drone",
|
|
85
88
|
"service": "drone",
|
|
86
|
-
"commit": "
|
|
87
|
-
"build": "
|
|
88
|
-
"branch": "
|
|
89
|
+
"commit": "ca878f0f32cf4eaf0ebd53f2467973e5c59e8b97",
|
|
90
|
+
"build": "1407",
|
|
91
|
+
"branch": "3.x",
|
|
89
92
|
"isPr": false,
|
|
90
93
|
"slug": "livingdocsIO/editable.js",
|
|
91
|
-
"date": "
|
|
94
|
+
"date": "2024-01-10T22:49:33.014Z"
|
|
92
95
|
}
|
|
93
96
|
}
|
package/src/block.js
CHANGED
|
@@ -7,11 +7,12 @@ const state = {}
|
|
|
7
7
|
export const next = getSibling('nextElementSibling')
|
|
8
8
|
export const previous = getSibling('previousElementSibling')
|
|
9
9
|
|
|
10
|
-
export function init (elem, {normalize, shouldSpellcheck}) {
|
|
10
|
+
export function init (elem, {normalize, plainText, shouldSpellcheck}) {
|
|
11
11
|
setBlockId(elem)
|
|
12
12
|
|
|
13
13
|
elem.setAttribute('contenteditable', true)
|
|
14
14
|
elem.setAttribute('spellcheck', Boolean(shouldSpellcheck))
|
|
15
|
+
elem.setAttribute('data-plaintext', Boolean(plainText))
|
|
15
16
|
|
|
16
17
|
elem.classList.remove(config.editableDisabledClass)
|
|
17
18
|
elem.classList.add(config.editableClass)
|
|
@@ -23,6 +24,7 @@ export function init (elem, {normalize, shouldSpellcheck}) {
|
|
|
23
24
|
export function disable (elem) {
|
|
24
25
|
elem.removeAttribute('contenteditable')
|
|
25
26
|
elem.removeAttribute('spellcheck')
|
|
27
|
+
elem.removeAttribute('data-plaintext')
|
|
26
28
|
|
|
27
29
|
setState(elem, undefined)
|
|
28
30
|
|
|
@@ -30,6 +32,9 @@ export function disable (elem) {
|
|
|
30
32
|
elem.classList.add(config.editableDisabledClass)
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
export function isPlainTextBlock (elem) {
|
|
36
|
+
return elem.getAttribute('data-plaintext') === 'true'
|
|
37
|
+
}
|
|
33
38
|
|
|
34
39
|
export function setBlockId (elem) {
|
|
35
40
|
if (!elem.hasAttribute('data-editable')) {
|
package/src/clipboard.js
CHANGED
|
@@ -2,8 +2,9 @@ import config from './config'
|
|
|
2
2
|
import * as string from './util/string'
|
|
3
3
|
import * as nodeType from './node-type'
|
|
4
4
|
import * as quotes from './quotes'
|
|
5
|
+
import {isPlainTextBlock} from './block'
|
|
5
6
|
|
|
6
|
-
let allowedElements, requiredAttributes, transformElements, blockLevelElements, replaceQuotes
|
|
7
|
+
let allowedElements, allowedPlainTextElements, requiredAttributes, transformElements, blockLevelElements, replaceQuotes
|
|
7
8
|
let splitIntoBlocks, blacklistedElements
|
|
8
9
|
const whitespaceOnly = /^\s*$/
|
|
9
10
|
const blockPlaceholder = '<!-- BLOCK -->'
|
|
@@ -13,6 +14,7 @@ updateConfig(config)
|
|
|
13
14
|
export function updateConfig (conf) {
|
|
14
15
|
const rules = conf.pastedHtmlRules
|
|
15
16
|
allowedElements = rules.allowedElements || {}
|
|
17
|
+
allowedPlainTextElements = rules.allowedPlainTextElements || {}
|
|
16
18
|
requiredAttributes = rules.requiredAttributes || {}
|
|
17
19
|
transformElements = rules.transformElements || {}
|
|
18
20
|
blacklistedElements = rules.blacklistedElements || []
|
|
@@ -25,9 +27,9 @@ export function updateConfig (conf) {
|
|
|
25
27
|
rules.splitIntoBlocks.forEach((name) => { splitIntoBlocks[name] = true })
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
export function paste (
|
|
29
|
-
const document =
|
|
30
|
-
|
|
30
|
+
export function paste (block, cursor, clipboardContent) {
|
|
31
|
+
const document = block.ownerDocument
|
|
32
|
+
block.setAttribute(config.pastingAttribute, true)
|
|
31
33
|
|
|
32
34
|
if (cursor.isSelection) {
|
|
33
35
|
cursor = cursor.deleteExactSurroundingTags()
|
|
@@ -39,9 +41,10 @@ export function paste (element, cursor, clipboardContent) {
|
|
|
39
41
|
const pasteHolder = document.createElement('div')
|
|
40
42
|
pasteHolder.innerHTML = clipboardContent
|
|
41
43
|
|
|
42
|
-
const
|
|
44
|
+
const isPlainText = isPlainTextBlock(block)
|
|
45
|
+
const blocks = parseContent(pasteHolder, {plainText: isPlainText})
|
|
43
46
|
|
|
44
|
-
|
|
47
|
+
block.removeAttribute(config.pastingAttribute)
|
|
45
48
|
return {blocks, cursor}
|
|
46
49
|
}
|
|
47
50
|
|
|
@@ -55,30 +58,35 @@ export function paste (element, cursor, clipboardContent) {
|
|
|
55
58
|
* @param {DOM node} A container where the pasted content is located.
|
|
56
59
|
* @returns {Array of Strings} An array of cleaned innerHTML like strings.
|
|
57
60
|
*/
|
|
58
|
-
export function parseContent (element) {
|
|
61
|
+
export function parseContent (element, {plainText = false} = {}) {
|
|
62
|
+
const options = {
|
|
63
|
+
allowedElements: plainText ? allowedPlainTextElements : allowedElements,
|
|
64
|
+
keepInternalRelativeLinks: plainText ? false : keepInternalRelativeLinks
|
|
65
|
+
}
|
|
66
|
+
|
|
59
67
|
// Filter pasted content
|
|
60
|
-
return filterHtmlElements(element)
|
|
68
|
+
return filterHtmlElements(element, options)
|
|
61
69
|
// Handle Blocks
|
|
62
70
|
.split(blockPlaceholder)
|
|
63
71
|
.map((entry) => string.trim(cleanWhitespace(replaceAllQuotes(entry))))
|
|
64
72
|
.filter((entry) => !whitespaceOnly.test(entry))
|
|
65
73
|
}
|
|
66
74
|
|
|
67
|
-
|
|
75
|
+
function filterHtmlElements (elem, options) {
|
|
68
76
|
return Array.from(elem.childNodes).reduce((content, child) => {
|
|
69
77
|
if (blacklistedElements.indexOf(child.nodeName.toLowerCase()) !== -1) {
|
|
70
78
|
return ''
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
// Keep internal relative links relative (on paste).
|
|
74
|
-
if (keepInternalRelativeLinks && child.nodeName === 'A' && child.href) {
|
|
82
|
+
if (options.keepInternalRelativeLinks && child.nodeName === 'A' && child.href) {
|
|
75
83
|
const stripInternalHost = child.getAttribute('href').replace(window.location.origin, '')
|
|
76
84
|
child.setAttribute('href', stripInternalHost)
|
|
77
85
|
}
|
|
78
86
|
|
|
79
87
|
if (child.nodeType === nodeType.elementNode) {
|
|
80
|
-
const childContent = filterHtmlElements(child)
|
|
81
|
-
return content + conditionalNodeWrap(child, childContent)
|
|
88
|
+
const childContent = filterHtmlElements(child, options)
|
|
89
|
+
return content + conditionalNodeWrap(child, childContent, options)
|
|
82
90
|
}
|
|
83
91
|
|
|
84
92
|
// Escape HTML characters <, > and &
|
|
@@ -87,11 +95,11 @@ export function filterHtmlElements (elem) {
|
|
|
87
95
|
}, '')
|
|
88
96
|
}
|
|
89
97
|
|
|
90
|
-
|
|
98
|
+
function conditionalNodeWrap (child, content, options) {
|
|
91
99
|
let nodeName = child.nodeName.toLowerCase()
|
|
92
100
|
nodeName = transformNodeName(nodeName)
|
|
93
101
|
|
|
94
|
-
if (shouldKeepNode(nodeName, child)) {
|
|
102
|
+
if (shouldKeepNode(nodeName, child, options)) {
|
|
95
103
|
const attributes = filterAttributes(nodeName, child)
|
|
96
104
|
|
|
97
105
|
if (nodeName === 'br') return `<${nodeName + attributes}>`
|
|
@@ -115,7 +123,7 @@ export function conditionalNodeWrap (child, content) {
|
|
|
115
123
|
}
|
|
116
124
|
|
|
117
125
|
// returns string of concatenated attributes e.g. 'target="_blank" rel="nofollow" href="/test.com"'
|
|
118
|
-
|
|
126
|
+
function filterAttributes (nodeName, node) {
|
|
119
127
|
return Array.from(node.attributes).reduce((attributes, {name, value}) => {
|
|
120
128
|
if (allowedElements[nodeName][name] && value) {
|
|
121
129
|
return `${attributes} ${name}="${value}"`
|
|
@@ -124,22 +132,22 @@ export function filterAttributes (nodeName, node) {
|
|
|
124
132
|
}, '')
|
|
125
133
|
}
|
|
126
134
|
|
|
127
|
-
|
|
135
|
+
function transformNodeName (nodeName) {
|
|
128
136
|
return transformElements[nodeName] || nodeName
|
|
129
137
|
}
|
|
130
138
|
|
|
131
|
-
|
|
139
|
+
function hasRequiredAttributes (nodeName, node) {
|
|
132
140
|
const requiredAttrs = requiredAttributes[nodeName]
|
|
133
141
|
if (!requiredAttrs) return true
|
|
134
142
|
|
|
135
143
|
return !requiredAttrs.some((name) => !node.getAttribute(name))
|
|
136
144
|
}
|
|
137
145
|
|
|
138
|
-
|
|
139
|
-
return allowedElements[nodeName] && hasRequiredAttributes(nodeName, node)
|
|
146
|
+
function shouldKeepNode (nodeName, node, options) {
|
|
147
|
+
return options.allowedElements[nodeName] && hasRequiredAttributes(nodeName, node)
|
|
140
148
|
}
|
|
141
149
|
|
|
142
|
-
|
|
150
|
+
function cleanWhitespace (str) {
|
|
143
151
|
return str
|
|
144
152
|
.replace(/\n/g, ' ')
|
|
145
153
|
.replace(/ {2,}/g, ' ')
|
|
@@ -149,7 +157,7 @@ export function cleanWhitespace (str) {
|
|
|
149
157
|
))
|
|
150
158
|
}
|
|
151
159
|
|
|
152
|
-
|
|
160
|
+
function replaceAllQuotes (str) {
|
|
153
161
|
if (replaceQuotes.quotes || replaceQuotes.singleQuotes || replaceQuotes.apostrophe) {
|
|
154
162
|
return quotes.replaceAllQuotes(str, replaceQuotes)
|
|
155
163
|
}
|
package/src/config.js
CHANGED
package/src/core.js
CHANGED
|
@@ -93,10 +93,11 @@ export class Editable {
|
|
|
93
93
|
* @param {HTMLElement|Array(HTMLElement)|String} target A HTMLElement, an
|
|
94
94
|
* array of HTMLElement or a query selector representing the target where
|
|
95
95
|
* the API should be added on.
|
|
96
|
+
* @param {normalize, plainText} options Block specific configuration
|
|
96
97
|
* @chainable
|
|
97
98
|
*/
|
|
98
|
-
add (target) {
|
|
99
|
-
this.enable(target)
|
|
99
|
+
add (target, options) {
|
|
100
|
+
this.enable(target, options)
|
|
100
101
|
// TODO check css whitespace settings
|
|
101
102
|
return this
|
|
102
103
|
}
|
|
@@ -143,19 +144,26 @@ export class Editable {
|
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
/**
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
147
|
+
* Adds the Editable.JS API to the given target elements.
|
|
148
|
+
*
|
|
149
|
+
* @method enable
|
|
150
|
+
* @param { HTMLElement | undefined } target editable root element(s)
|
|
151
|
+
* If no param is specified all editables marked as disabled are enabled.
|
|
152
|
+
* @param {boolean} normalize normalizes target content (legacy param)
|
|
153
|
+
* @param {boolean} options.normalize normalizes target content
|
|
154
|
+
* @param {boolean} options.plainText prevents text formatting for block
|
|
155
|
+
* @chainable
|
|
156
|
+
*/
|
|
157
|
+
enable (target, options) {
|
|
158
|
+
const {
|
|
159
|
+
normalize = typeof options === 'boolean' ? options : false,
|
|
160
|
+
plainText = false
|
|
161
|
+
} = options ?? {}
|
|
154
162
|
const shouldSpellcheck = this.config.browserSpellcheck
|
|
155
163
|
const targets = domArray(target || `.${config.editableDisabledClass}`, this.win.document)
|
|
156
164
|
|
|
157
165
|
for (const element of targets) {
|
|
158
|
-
block.init(element, {normalize, shouldSpellcheck})
|
|
166
|
+
block.init(element, {normalize, plainText, shouldSpellcheck})
|
|
159
167
|
this.dispatcher.notify('init', element)
|
|
160
168
|
}
|
|
161
169
|
|
package/src/selection.js
CHANGED
|
@@ -3,9 +3,14 @@ import 'rangy/lib/rangy-textrange'
|
|
|
3
3
|
import Cursor from './cursor'
|
|
4
4
|
import * as content from './content'
|
|
5
5
|
import * as parser from './parser'
|
|
6
|
+
import * as block from './block'
|
|
6
7
|
import config from './config'
|
|
7
8
|
import highlightSupport from './highlight-support'
|
|
8
9
|
import highlightText from './highlight-text'
|
|
10
|
+
import {
|
|
11
|
+
findStartExcludingWhitespace,
|
|
12
|
+
findEndExcludingWhitespace
|
|
13
|
+
} from './util/dom'
|
|
9
14
|
|
|
10
15
|
/**
|
|
11
16
|
* The Selection module provides a cross-browser abstraction layer for range
|
|
@@ -97,9 +102,25 @@ export default class Selection extends Cursor {
|
|
|
97
102
|
const textToTrim = this.range.toString()
|
|
98
103
|
const whitespacesOnTheLeft = textToTrim.search(/\S|$/)
|
|
99
104
|
const lastNonWhitespace = textToTrim.search(/\S[\s]+$/)
|
|
100
|
-
const whitespacesOnTheRight = lastNonWhitespace === -1
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
const whitespacesOnTheRight = lastNonWhitespace === -1
|
|
106
|
+
? 0
|
|
107
|
+
: textToTrim.length - (lastNonWhitespace + 1)
|
|
108
|
+
|
|
109
|
+
const [startContainer, startOffset] = findStartExcludingWhitespace({
|
|
110
|
+
root: this.range.commonAncestorContainer,
|
|
111
|
+
startContainer: this.range.startContainer,
|
|
112
|
+
startOffset: this.range.startOffset,
|
|
113
|
+
whitespacesOnTheLeft
|
|
114
|
+
})
|
|
115
|
+
this.range.setStart(startContainer, startOffset)
|
|
116
|
+
|
|
117
|
+
const [endContainer, endOffset] = findEndExcludingWhitespace({
|
|
118
|
+
root: this.range.commonAncestorContainer,
|
|
119
|
+
endContainer: this.range.endContainer,
|
|
120
|
+
endOffset: this.range.endOffset,
|
|
121
|
+
whitespacesOnTheRight
|
|
122
|
+
})
|
|
123
|
+
this.range.setEnd(endContainer, endOffset)
|
|
103
124
|
}
|
|
104
125
|
|
|
105
126
|
unlink () {
|
|
@@ -144,6 +165,8 @@ export default class Selection extends Cursor {
|
|
|
144
165
|
|
|
145
166
|
// toggle('<em>')
|
|
146
167
|
toggle (elem) {
|
|
168
|
+
if (block.isPlainTextBlock(this.host)) return
|
|
169
|
+
if (this.range.collapsed) return
|
|
147
170
|
elem = this.adoptElement(elem)
|
|
148
171
|
this.range = content.toggleTag(this.host, this.range, elem)
|
|
149
172
|
this.setSelection()
|
|
@@ -307,6 +330,8 @@ export default class Selection extends Cursor {
|
|
|
307
330
|
// the same tagName is affecting the selection this tag will be
|
|
308
331
|
// remove first.
|
|
309
332
|
forceWrap (elem) {
|
|
333
|
+
if (block.isPlainTextBlock(this.host)) return
|
|
334
|
+
if (this.range.collapsed) return
|
|
310
335
|
elem = this.adoptElement(elem)
|
|
311
336
|
this.range = content.forceWrap(this.host, this.range, elem)
|
|
312
337
|
this.setSelection()
|
package/src/util/dom.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import NodeIterator from '../node-iterator'
|
|
2
|
+
import {textNode} from '../node-type'
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* @param {HTMLElement | Array | String} target
|
|
3
6
|
* @param {Document} document
|
|
@@ -42,3 +45,88 @@ export const closest = (elem, selector) => {
|
|
|
42
45
|
if (!elem.closest) elem = elem.parentNode
|
|
43
46
|
if (elem && elem.closest) return elem.closest(selector)
|
|
44
47
|
}
|
|
48
|
+
|
|
49
|
+
export function findStartExcludingWhitespace ({root, startContainer, startOffset, whitespacesOnTheLeft}) {
|
|
50
|
+
const isTextNode = startContainer.nodeType === textNode
|
|
51
|
+
if (!isTextNode) {
|
|
52
|
+
return findStartExcludingWhitespace({
|
|
53
|
+
root,
|
|
54
|
+
startContainer: startContainer.childNodes[startOffset],
|
|
55
|
+
startOffset: 0,
|
|
56
|
+
whitespacesOnTheLeft
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const offsetAfterWhitespace = startOffset + whitespacesOnTheLeft
|
|
61
|
+
if (startContainer.length > offsetAfterWhitespace) {
|
|
62
|
+
return [startContainer, offsetAfterWhitespace]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Pass the root so that the iterator can traverse to siblings
|
|
66
|
+
const iterator = new NodeIterator(root)
|
|
67
|
+
// Set the position to the node which is selected
|
|
68
|
+
iterator.nextNode = startContainer
|
|
69
|
+
// Iterate once to avoid returning self
|
|
70
|
+
iterator.getNextTextNode()
|
|
71
|
+
|
|
72
|
+
const container = iterator.getNextTextNode()
|
|
73
|
+
if (!container) {
|
|
74
|
+
// No more text nodes - use the end of the last text node
|
|
75
|
+
const previousTextNode = iterator.getPreviousTextNode()
|
|
76
|
+
return [previousTextNode, previousTextNode.length]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return findStartExcludingWhitespace({
|
|
80
|
+
root,
|
|
81
|
+
startContainer: container,
|
|
82
|
+
startOffset: 0,
|
|
83
|
+
whitespacesOnTheLeft: offsetAfterWhitespace - startContainer.length
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function findEndExcludingWhitespace ({root, endContainer, endOffset, whitespacesOnTheRight}) {
|
|
88
|
+
const isTextNode = endContainer.nodeType === textNode
|
|
89
|
+
if (!isTextNode) {
|
|
90
|
+
const isFirstNode = !endContainer.childNodes[endOffset - 1]
|
|
91
|
+
const container = isFirstNode
|
|
92
|
+
? endContainer.childNodes[endOffset]
|
|
93
|
+
: endContainer.childNodes[endOffset - 1]
|
|
94
|
+
let offset = 0
|
|
95
|
+
if (!isFirstNode) {
|
|
96
|
+
offset = container.nodeType === textNode
|
|
97
|
+
? container.length
|
|
98
|
+
: container.childNodes.length
|
|
99
|
+
}
|
|
100
|
+
return findEndExcludingWhitespace({
|
|
101
|
+
root,
|
|
102
|
+
endContainer: container,
|
|
103
|
+
endOffset: offset,
|
|
104
|
+
whitespacesOnTheRight
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const offsetBeforeWhitespace = endOffset - whitespacesOnTheRight
|
|
109
|
+
if (offsetBeforeWhitespace > 0) {
|
|
110
|
+
return [endContainer, offsetBeforeWhitespace]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Pass the root so that the iterator can traverse to siblings
|
|
114
|
+
const iterator = new NodeIterator(root)
|
|
115
|
+
// Set the position to the node which is selected
|
|
116
|
+
iterator.previous = endContainer
|
|
117
|
+
// Iterate once to avoid returning self
|
|
118
|
+
iterator.getPreviousTextNode()
|
|
119
|
+
|
|
120
|
+
const container = iterator.getPreviousTextNode()
|
|
121
|
+
if (!container) {
|
|
122
|
+
// No more text nodes - use the start of the first text node
|
|
123
|
+
return [iterator.getNextTextNode(), 0]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return findEndExcludingWhitespace({
|
|
127
|
+
root,
|
|
128
|
+
endContainer: container,
|
|
129
|
+
endOffset: container.length,
|
|
130
|
+
whitespacesOnTheRight: whitespacesOnTheRight - endOffset
|
|
131
|
+
})
|
|
132
|
+
}
|