Skip to content

Commit 82c54fc

Browse files
peyerlukgabrielhase
authored andcommitted
code(cursor-search): fix inconsistencies, simplify logic
1 parent 1853a62 commit 82c54fc

File tree

9 files changed

+269
-198
lines changed

9 files changed

+269
-198
lines changed

spec/api.spec.js

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,11 @@ describe('Editable', function () {
126126

127127
describe('findClosestCursorOffset:', function () {
128128
/*
129-
Cursor1: | (left: 130)
130-
Comp 1: Cristiano Ronaldo wurde 2018 mit grossem Tamtam nach Turin geholt.
131-
Comp 2: Der Spieler blieb bei fünf Champions-League-Titeln stehen.
132-
Cursor 2: | (offset: 19 chars)
133-
*/
129+
* Cursor1: | (left: 130)
130+
* Comp 1: Cristiano Ronaldo wurde 2018 mit grossem Tamtam nach Turin geholt.
131+
* Comp 2: Der Spieler blieb bei fünf Champions-League-Titeln stehen.
132+
* Cursor 2: | (offset: 19 chars)
133+
*/
134134
it('finds the index in a text node', function () {
135135
$div.html('Der Spieler blieb bei fünf Champions-League-Titeln stehen.')
136136
const {wasFound, offset} = editable.findClosestCursorOffset({
@@ -142,11 +142,11 @@ Cursor 2: | (offset: 19 chars)
142142
})
143143

144144
/*
145-
Cursor1: | (left: 130)
146-
Comp 1: Cristiano Ronaldo wurde 2018 mit grossem Tamtam nach Turin geholt.
147-
Comp 2: <p>Der <em>Spieler</em> blieb bei fünf <span>Champions-League-Titeln</span> stehen.</p>
148-
Cursor 2: |
149-
*/
145+
* Cursor1: | (left: 130)
146+
* Comp 1: Cristiano Ronaldo wurde 2018 mit grossem Tamtam nach Turin geholt.
147+
* Comp 2: <p>Der <em>Spieler</em> blieb bei fünf <span>Champions-League-Titeln</span> stehen.</p>
148+
* Cursor 2: |
149+
*/
150150
it('finds the index in a nested html tag structure', function () {
151151
$div.html('<p>Der <em>Spieler</em> blieb bei fünf <span>Champions-League-Titeln</span> stehen.</p>')
152152
const {wasFound, offset} = editable.findClosestCursorOffset({
@@ -167,18 +167,19 @@ Cursor 2: |
167167
})
168168

169169
/*
170-
Cursor1: |
171-
Comp 1: Cristiano Ronaldo wurde 2018 mit grossem Tamtam nach Turin geholt.
172-
Comp 2: Foo
173-
Cursor 2: not found
174-
*/
170+
* Cursor1: |
171+
* Comp 1: Cristiano Ronaldo wurde 2018 mit grossem Tamtam nach Turin geholt.
172+
* Comp 2: Foo
173+
* Cursor 2: not found
174+
*/
175175
it('returns not found for coordinates that are out of the text area', function () {
176176
$div.html('Foo')
177-
const {wasFound} = editable.findClosestCursorOffset({
177+
const {wasFound, offset} = editable.findClosestCursorOffset({
178178
element: $div[0],
179179
origCoordinates: {top: 0, left: 130}
180180
})
181-
expect(wasFound).toEqual(false)
181+
expect(wasFound).toEqual(true)
182+
expect(offset).toEqual(3)
182183
})
183184
})
184185
})

spec/node-iterator.spec.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ describe('NodeIterator', function () {
2222
it('sets its properties', function () {
2323
expect(this.iterator.root).toEqual(this.element)
2424
expect(this.iterator.current).toEqual(this.element)
25-
expect(this.iterator.next).toEqual(this.element)
25+
expect(this.iterator.nextNode).toEqual(this.element)
26+
expect(this.iterator.previous).toEqual(this.element)
2627
})
2728
})
2829

@@ -63,7 +64,7 @@ describe('NodeIterator', function () {
6364

6465
this.iterator.replaceCurrent(replacement)
6566
expect(this.iterator.current).toEqual(replacement)
66-
expect(this.iterator.next).toEqual(null)
67+
expect(this.iterator.nextNode).toEqual(null)
6768
})
6869

6970
it('replaces the first character of longer a text node', function () {

src/core.js

Lines changed: 44 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import highlightSupport from './highlight-support'
1212
import Highlighting from './highlighting'
1313
import createDefaultEvents from './create-default-events'
1414
import browser from 'bowser'
15-
import {getTotalCharCount, textNodesUnder, getTextNodeAndRelativeOffset} from './util/element'
15+
import {textNodesUnder, getTextNodeAndRelativeOffset} from './util/element'
1616
import {binaryCursorSearch} from './util/binary_search'
1717

1818
/**
@@ -195,16 +195,15 @@ export default class Editable {
195195
*/
196196

197197
createCursor (element, position = 'beginning') {
198-
const $host = $(element).closest(this.editableSelector)
199-
200-
if (!$host.length) return undefined
198+
const host = Cursor.findHost(element, this.editableSelector)
199+
if (!host) return undefined
201200

202201
const range = rangy.createRange()
203202

204203
if (position === 'beginning' || position === 'end') {
205204
range.selectNodeContents(element)
206205
range.collapse(position === 'beginning')
207-
} else if (element !== $host[0]) {
206+
} else if (element !== host) {
208207
if (position === 'before') {
209208
range.setStartBefore(element)
210209
range.setEndBefore(element)
@@ -216,26 +215,19 @@ export default class Editable {
216215
error('EditableJS: cannot create cursor outside of an editable block.')
217216
}
218217

219-
return new Cursor($host[0], range)
220-
}
221-
222-
createRangyRange () {
223-
return rangy.createRange()
224-
}
225-
226-
createCursorWithRange ({element, range}) {
227-
const $host = $(element).closest(this.editableSelector)
228-
return new Cursor($host[0], range)
218+
return new Cursor(host, range)
229219
}
230220

231221
createCursorAtCharacterOffset ({element, offset}) {
232222
const textNodes = textNodesUnder(element)
233223
const {node, relativeOffset} = getTextNodeAndRelativeOffset({textNodes, absOffset: offset})
234-
const newRange = this.createRangyRange()
224+
const newRange = rangy.createRange()
235225
newRange.setStart(node, relativeOffset)
236-
newRange.setEnd(node, relativeOffset)
237-
newRange.collapse()
238-
const nextCursor = this.createCursorWithRange({element, range: newRange})
226+
newRange.collapse(true)
227+
228+
const host = Cursor.findHost(element, this.editableSelector)
229+
const nextCursor = new Cursor(host, newRange)
230+
239231
nextCursor.setVisibleSelection()
240232
return nextCursor
241233
}
@@ -470,83 +462,42 @@ export default class Editable {
470462
return this
471463
}
472464

473-
/*
474-
Takes coordinates and uses its left value to find out how to offset a character in a string to
475-
closely match the coordinates.left value.
476-
Takes conditions for the result being on the first line, used when navigating to a paragraph from
477-
the above paragraph and being on the last line, used when navigating to a paragraph from the below
478-
paragraph.
479-
480-
Internally this sets up the methods used for a binary cursor search and calls this.
481-
482-
@param {DomNode} element The DOM Node (usually editable elem) to which the cursor jumps
483-
@param {Object} coordinates The bounding rect of the preceeding cursor to be matched
484-
@param {Boolean} requiredOnFirstLine set to true if you want to require the cursor to be on the first line of the paragraph
485-
@param {Boolean} requiredOnLastLine set to true if you want to require the cursor to be on the last line of the paragraph
486-
487-
@return {Object} object with boolean `wasFound` indicating if the binary search found an offset and `offset` to indicate the actual character offset
488-
*/
489-
findClosestCursorOffset (
490-
{element, origCoordinates, requiredOnFirstLine = false, requiredOnLastLine = false}) {
491-
// early terminate on empty editables
492-
const totalCharCount = getTotalCharCount(element)
493-
if (totalCharCount === 0) return {wasFound: false}
494-
// move left if the coordinates are to the left
495-
const leftCondition = ({data, coordinates}) => {
496-
if (coordinates.left >= data.origCoordinates.left) return true
497-
return false
498-
}
499-
// move up if cursor is required to be on first line but is currently not
500-
const upCondition = ({data, cursor}) => {
501-
if (data.requiredOnFirstLine && !cursor.isAtFirstLine()) return true
502-
return false
503-
}
504-
// move down if cursor is required to be on last line but is currently not
505-
const downCondition = ({data, cursor}) => {
506-
if (data.requiredOnLastLine && !cursor.isAtLastLine()) return true
507-
return false
508-
}
509-
// move the binary search index in between the current position and the left limit
510-
const moveLeft = (data) => {
511-
data.rightLimit = data.currentOffset
512-
data.currentOffset = Math.floor((data.currentOffset - data.leftLimit) / 2)
513-
}
514-
// move the binary search index in between the current position and the right limit
515-
const moveRight = (data) => {
516-
data.leftLimit = data.currentOffset
517-
data.currentOffset = data.currentOffset + Math.floor((data.rightLimit - data.currentOffset) / 2)
518-
}
519-
// consider a small bluriness since character never exactly match coordinates
520-
const foundChecker = ({distance, bluriness}) => {
521-
return distance <= bluriness
522-
}
523-
// consider converged if 2 consecutive history entries are equal
524-
const convergenceChecker = (history) => {
525-
const lastTwo = history.slice(-2)
526-
if (lastTwo.length === 2 && lastTwo[0] === lastTwo[1]) return true
527-
return false
528-
}
465+
/**
466+
* Takes coordinates and uses its left value to find out how to offset a character in a string to
467+
* closely match the coordinates.left value.
468+
* Takes conditions for the result being on the first line, used when navigating to a paragraph from
469+
* the above paragraph and being on the last line, used when navigating to a paragraph from the below
470+
* paragraph.
471+
*
472+
* Internally this sets up the methods used for a binary cursor search and calls this.
473+
*
474+
* @param {DomNode} element
475+
* - the editable hostDOM Node to which the cursor jumps
476+
* @param {object} coordinates
477+
* - The bounding rect of the preceeding cursor to be matched
478+
* @param {boolean} requiredOnFirstLine
479+
* - set to true if you want to require the cursor to be on the first line of the paragraph
480+
* @param {boolean} requiredOnLastLine
481+
* - set to true if you want to require the cursor to be on the last line of the paragraph
482+
*
483+
* @return {Object}
484+
* - object with boolean `wasFound` indicating if the binary search found an offset and `offset` to indicate the actual character offset
485+
*/
486+
findClosestCursorOffset ({
487+
element,
488+
origCoordinates,
489+
requiredOnFirstLine = false,
490+
requiredOnLastLine = false
491+
}) {
492+
const positionX = this.dispatcher.switchContext
493+
? this.dispatcher.switchContext.positionX
494+
: origCoordinates.left
529495

530-
const data = {
531-
element,
532-
currentOffset: Math.floor(totalCharCount / 2),
533-
rightLimit: totalCharCount,
534-
leftLimit: 0,
496+
return binaryCursorSearch({
497+
host: element,
535498
requiredOnFirstLine,
536499
requiredOnLastLine,
537-
origCoordinates
538-
}
539-
540-
return binaryCursorSearch({
541-
moveLeft,
542-
moveRight,
543-
leftCondition,
544-
upCondition,
545-
downCondition,
546-
convergenceChecker,
547-
foundChecker,
548-
createCursorAtCharacterOffset: this.createCursorAtCharacterOffset.bind(this),
549-
data
500+
positionX
550501
})
551502
}
552503
}

src/cursor.js

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import * as viewport from './util/viewport'
55
import * as content from './content'
66
import * as parser from './parser'
77
import * as string from './util/string'
8-
import * as nodeType from './node-type'
8+
import {elementNode, documentFragmentNode} from './node-type'
99
import error from './util/error'
1010
import * as rangeSaveRestore from './range-save-restore'
11+
// import printRange from './util/print_range'
12+
import NodeIterator from './node-iterator'
1113

1214
/**
1315
* The Cursor module provides a cross-browser abstraction layer for cursor.
@@ -17,6 +19,12 @@ import * as rangeSaveRestore from './range-save-restore'
1719
*/
1820

1921
export default class Cursor {
22+
23+
static findHost (elem, selector) {
24+
if (!elem.closest) elem = elem.parentNode
25+
return elem.closest(selector)
26+
}
27+
2028
/**
2129
* Class for the Cursor module.
2230
*
@@ -46,21 +54,61 @@ export default class Cursor {
4654
}
4755

4856
isAtLastLine () {
49-
const range = rangy.createRange()
50-
range.selectNodeContents(this.host)
51-
52-
const hostCoords = range.nativeRange.getBoundingClientRect()
53-
const cursorCoords = this.range.nativeRange.getBoundingClientRect()
57+
const hostRange = this.win.document.createRange()
58+
hostRange.selectNodeContents(this.host)
59+
const hostCoords = hostRange.getBoundingClientRect()
60+
61+
let cursorCoords
62+
if (this.range.nativeRange.startContainer.nodeType === elementNode) {
63+
const container = this.range.nativeRange.startContainer
64+
if ((container.children.length - 1) >= this.range.nativeRange.startOffset) {
65+
const elem = container.children[this.range.nativeRange.startOffset]
66+
const iterator = new NodeIterator(elem)
67+
const textNode = iterator.getNextTextNode()
68+
if (textNode) {
69+
const cursorRange = this.win.document.createRange()
70+
cursorRange.setStart(textNode, 0)
71+
cursorRange.collapse(true)
72+
cursorCoords = cursorRange.getBoundingClientRect()
73+
} else {
74+
cursorCoords = hostCoords
75+
}
76+
} else {
77+
cursorCoords = hostCoords
78+
}
79+
} else {
80+
cursorCoords = this.getBoundingClientRect()
81+
}
5482

5583
return hostCoords.bottom === cursorCoords.bottom
5684
}
5785

5886
isAtFirstLine () {
59-
const range = rangy.createRange()
60-
range.selectNodeContents(this.host)
61-
62-
const hostCoords = range.nativeRange.getBoundingClientRect()
63-
const cursorCoords = this.range.nativeRange.getBoundingClientRect()
87+
const hostRange = this.win.document.createRange()
88+
hostRange.selectNodeContents(this.host)
89+
const hostCoords = hostRange.getBoundingClientRect()
90+
91+
let cursorCoords
92+
if (this.range.nativeRange.startContainer.nodeType === elementNode) {
93+
const container = this.range.nativeRange.startContainer
94+
if ((container.children.length - 1) >= this.range.nativeRange.startOffset) {
95+
const elem = container.children[this.range.nativeRange.startOffset]
96+
const iterator = new NodeIterator(elem)
97+
const textNode = iterator.getPreviousTextNode()
98+
if (textNode) {
99+
const cursorRange = this.win.document.createRange()
100+
cursorRange.setStart(textNode, 0)
101+
cursorRange.collapse(true)
102+
cursorCoords = cursorRange.getBoundingClientRect()
103+
} else {
104+
cursorCoords = hostCoords
105+
}
106+
} else {
107+
cursorCoords = hostCoords
108+
}
109+
} else {
110+
cursorCoords = this.range.nativeRange.getBoundingClientRect()
111+
}
64112

65113
return hostCoords.top === cursorCoords.top
66114
}
@@ -83,7 +131,7 @@ export default class Cursor {
83131
element = this.adoptElement(element)
84132

85133
let preceedingElement = element
86-
if (element.nodeType === nodeType.documentFragmentNode) {
134+
if (element.nodeType === documentFragmentNode) {
87135
const lastIndex = element.childNodes.length - 1
88136
preceedingElement = element.childNodes[lastIndex]
89137
}
@@ -169,6 +217,10 @@ export default class Cursor {
169217
return content.getInnerHtmlOfFragment(this.after())
170218
}
171219

220+
getBoundingClientRect () {
221+
return this.range.nativeRange.getBoundingClientRect()
222+
}
223+
172224
// Get the BoundingClientRect of the cursor.
173225
// The returned values are transformed to be absolute
174226
// (relative to the document).

0 commit comments

Comments
 (0)