@livingdocs/editable.js 5.0.0 → 5.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 -5
- package/src/selection.js +27 -4
- package/src/util/dom.js +91 -4
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livingdocs/editable.js",
|
|
3
3
|
"description": "Friendly contenteditable API",
|
|
4
|
-
"version": "5.0.
|
|
4
|
+
"version": "5.0.2",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@babel/runtime": "^7.16.7",
|
|
7
7
|
"lodash-es": "^4.17.21"
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"react-dom": "^18.0.0",
|
|
35
35
|
"react-transition-group": "^4.4.2",
|
|
36
36
|
"rimraf": "^5.0.0",
|
|
37
|
-
"sinon": "^
|
|
37
|
+
"sinon": "^17.0.0",
|
|
38
38
|
"style-loader": "^3.3.1",
|
|
39
39
|
"url-loader": "^4.1.1",
|
|
40
40
|
"webpack": "^5.68.0",
|
|
@@ -80,11 +80,11 @@
|
|
|
80
80
|
"isCi": true,
|
|
81
81
|
"name": "Drone",
|
|
82
82
|
"service": "drone",
|
|
83
|
-
"commit": "
|
|
84
|
-
"build": "
|
|
83
|
+
"commit": "5559989bbfcd372008fb892f10b29f3f55ed0077",
|
|
84
|
+
"build": "1390",
|
|
85
85
|
"branch": "master",
|
|
86
86
|
"isPr": false,
|
|
87
87
|
"slug": "livingdocsIO/editable.js",
|
|
88
|
-
"date": "2023-
|
|
88
|
+
"date": "2023-12-27T11:21:20.082Z"
|
|
89
89
|
}
|
|
90
90
|
}
|
package/src/selection.js
CHANGED
|
@@ -5,7 +5,12 @@ import * as block from './block'
|
|
|
5
5
|
import config from './config'
|
|
6
6
|
import highlightSupport from './highlight-support'
|
|
7
7
|
import highlightText from './highlight-text'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
toCharacterRange,
|
|
10
|
+
rangeToHtml,
|
|
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
|
|
@@ -96,9 +101,25 @@ export default class Selection extends Cursor {
|
|
|
96
101
|
const textToTrim = this.range.toString()
|
|
97
102
|
const whitespacesOnTheLeft = textToTrim.search(/\S|$/)
|
|
98
103
|
const lastNonWhitespace = textToTrim.search(/\S[\s]+$/)
|
|
99
|
-
const whitespacesOnTheRight = lastNonWhitespace === -1
|
|
100
|
-
|
|
101
|
-
|
|
104
|
+
const whitespacesOnTheRight = lastNonWhitespace === -1
|
|
105
|
+
? 0
|
|
106
|
+
: textToTrim.length - (lastNonWhitespace + 1)
|
|
107
|
+
|
|
108
|
+
const [startContainer, startOffset] = findStartExcludingWhitespace({
|
|
109
|
+
root: this.range.commonAncestorContainer,
|
|
110
|
+
startContainer: this.range.startContainer,
|
|
111
|
+
startOffset: this.range.startOffset,
|
|
112
|
+
whitespacesOnTheLeft
|
|
113
|
+
})
|
|
114
|
+
this.range.setStart(startContainer, startOffset)
|
|
115
|
+
|
|
116
|
+
const [endContainer, endOffset] = findEndExcludingWhitespace({
|
|
117
|
+
root: this.range.commonAncestorContainer,
|
|
118
|
+
endContainer: this.range.endContainer,
|
|
119
|
+
endOffset: this.range.endOffset,
|
|
120
|
+
whitespacesOnTheRight
|
|
121
|
+
})
|
|
122
|
+
this.range.setEnd(endContainer, endOffset)
|
|
102
123
|
}
|
|
103
124
|
|
|
104
125
|
unlink () {
|
|
@@ -156,6 +177,7 @@ export default class Selection extends Cursor {
|
|
|
156
177
|
// e.g. toggle('<em>')
|
|
157
178
|
toggle (elem) {
|
|
158
179
|
if (block.isPlainTextBlock(this.host)) return
|
|
180
|
+
if (this.range.collapsed) return
|
|
159
181
|
elem = this.adoptElement(elem)
|
|
160
182
|
this.range = content.toggleTag(this.host, this.range, elem)
|
|
161
183
|
this.setVisibleSelection()
|
|
@@ -320,6 +342,7 @@ export default class Selection extends Cursor {
|
|
|
320
342
|
// remove first.
|
|
321
343
|
forceWrap (elem) {
|
|
322
344
|
if (block.isPlainTextBlock(this.host)) return
|
|
345
|
+
if (this.range.collapsed) return
|
|
323
346
|
elem = this.adoptElement(elem)
|
|
324
347
|
this.range = content.forceWrap(this.host, this.range, elem)
|
|
325
348
|
this.setVisibleSelection()
|
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
|
|
@@ -228,8 +231,8 @@ export const createRangeFromCharacterRange = (element, actualStartIndex, actualE
|
|
|
228
231
|
let startNode, endNode, startOffset, endOffset
|
|
229
232
|
|
|
230
233
|
while (walker.nextNode()) {
|
|
231
|
-
const
|
|
232
|
-
const nodeLength =
|
|
234
|
+
const node = walker.currentNode
|
|
235
|
+
const nodeLength = node.nodeValue.length
|
|
233
236
|
|
|
234
237
|
if (currentIndex + nodeLength <= actualStartIndex) {
|
|
235
238
|
currentIndex += nodeLength
|
|
@@ -237,12 +240,12 @@ export const createRangeFromCharacterRange = (element, actualStartIndex, actualE
|
|
|
237
240
|
}
|
|
238
241
|
|
|
239
242
|
if (!startNode) {
|
|
240
|
-
startNode =
|
|
243
|
+
startNode = node
|
|
241
244
|
startOffset = actualStartIndex - currentIndex
|
|
242
245
|
}
|
|
243
246
|
|
|
244
247
|
if (currentIndex + nodeLength >= actualEndIndex) {
|
|
245
|
-
endNode =
|
|
248
|
+
endNode = node
|
|
246
249
|
endOffset = actualEndIndex - currentIndex
|
|
247
250
|
break
|
|
248
251
|
}
|
|
@@ -260,3 +263,87 @@ export const createRangeFromCharacterRange = (element, actualStartIndex, actualE
|
|
|
260
263
|
}
|
|
261
264
|
}
|
|
262
265
|
|
|
266
|
+
export function findStartExcludingWhitespace ({root, startContainer, startOffset, whitespacesOnTheLeft}) {
|
|
267
|
+
const isTextNode = startContainer.nodeType === textNode
|
|
268
|
+
if (!isTextNode) {
|
|
269
|
+
return findStartExcludingWhitespace({
|
|
270
|
+
root,
|
|
271
|
+
startContainer: startContainer.childNodes[startOffset],
|
|
272
|
+
startOffset: 0,
|
|
273
|
+
whitespacesOnTheLeft
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const offsetAfterWhitespace = startOffset + whitespacesOnTheLeft
|
|
278
|
+
if (startContainer.length > offsetAfterWhitespace) {
|
|
279
|
+
return [startContainer, offsetAfterWhitespace]
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Pass the root so that the iterator can traverse to siblings
|
|
283
|
+
const iterator = new NodeIterator(root)
|
|
284
|
+
// Set the position to the node which is selected
|
|
285
|
+
iterator.nextNode = startContainer
|
|
286
|
+
// Iterate once to avoid returning self
|
|
287
|
+
iterator.getNextTextNode()
|
|
288
|
+
|
|
289
|
+
const container = iterator.getNextTextNode()
|
|
290
|
+
if (!container) {
|
|
291
|
+
// No more text nodes - use the end of the last text node
|
|
292
|
+
const previousTextNode = iterator.getPreviousTextNode()
|
|
293
|
+
return [previousTextNode, previousTextNode.length]
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return findStartExcludingWhitespace({
|
|
297
|
+
root,
|
|
298
|
+
startContainer: container,
|
|
299
|
+
startOffset: 0,
|
|
300
|
+
whitespacesOnTheLeft: offsetAfterWhitespace - startContainer.length
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function findEndExcludingWhitespace ({root, endContainer, endOffset, whitespacesOnTheRight}) {
|
|
305
|
+
const isTextNode = endContainer.nodeType === textNode
|
|
306
|
+
if (!isTextNode) {
|
|
307
|
+
const isFirstNode = !endContainer.childNodes[endOffset - 1]
|
|
308
|
+
const container = isFirstNode
|
|
309
|
+
? endContainer.childNodes[endOffset]
|
|
310
|
+
: endContainer.childNodes[endOffset - 1]
|
|
311
|
+
let offset = 0
|
|
312
|
+
if (!isFirstNode) {
|
|
313
|
+
offset = container.nodeType === textNode
|
|
314
|
+
? container.length
|
|
315
|
+
: container.childNodes.length
|
|
316
|
+
}
|
|
317
|
+
return findEndExcludingWhitespace({
|
|
318
|
+
root,
|
|
319
|
+
endContainer: container,
|
|
320
|
+
endOffset: offset,
|
|
321
|
+
whitespacesOnTheRight
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const offsetBeforeWhitespace = endOffset - whitespacesOnTheRight
|
|
326
|
+
if (offsetBeforeWhitespace > 0) {
|
|
327
|
+
return [endContainer, offsetBeforeWhitespace]
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Pass the root so that the iterator can traverse to siblings
|
|
331
|
+
const iterator = new NodeIterator(root)
|
|
332
|
+
// Set the position to the node which is selected
|
|
333
|
+
iterator.previous = endContainer
|
|
334
|
+
// Iterate once to avoid returning self
|
|
335
|
+
iterator.getPreviousTextNode()
|
|
336
|
+
|
|
337
|
+
const container = iterator.getPreviousTextNode()
|
|
338
|
+
if (!container) {
|
|
339
|
+
// No more text nodes - use the start of the first text node
|
|
340
|
+
return [iterator.getNextTextNode(), 0]
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return findEndExcludingWhitespace({
|
|
344
|
+
root,
|
|
345
|
+
endContainer: container,
|
|
346
|
+
endOffset: container.length,
|
|
347
|
+
whitespacesOnTheRight: whitespacesOnTheRight - endOffset
|
|
348
|
+
})
|
|
349
|
+
}
|