Skip to content

Commit 00a092e

Browse files
committed
update to new selection handling
2 parents 26609b8 + 9273c8a commit 00a092e

31 files changed

+833
-454
lines changed

src/document/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,4 @@ function prepareElement(el: Node | HTMLInputElement) {
7676

7777
export {getUIValue, setUIValue, startTrackValue, endTrackValue} from './value'
7878
export {getUISelection, setUISelection} from './selection'
79+
export type {UISelectionRange} from './selection'

src/document/selection.ts

+54-20
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {getUIValue} from '.'
12
import {prepareInterceptor} from './interceptor'
23

34
const UISelection = Symbol('Displayed selection in UI')
@@ -6,9 +7,19 @@ interface Value extends Number {
67
[UISelection]?: typeof UISelection
78
}
89

10+
export interface UISelectionRange {
11+
startOffset: number
12+
endOffset: number
13+
}
14+
15+
export interface UISelection {
16+
anchorOffset: number
17+
focusOffset: number
18+
}
19+
920
declare global {
1021
interface Element {
11-
[UISelection]?: {start: number; end: number}
22+
[UISelection]?: UISelection
1223
}
1324
}
1425

@@ -26,9 +37,9 @@ export function prepareSelectionInterceptor(
2637
) {
2738
const isUI = start && typeof start === 'object' && start[UISelection]
2839

29-
this[UISelection] = isUI
30-
? {start: start.valueOf(), end: Number(end)}
31-
: undefined
40+
if (!isUI) {
41+
this[UISelection] = undefined
42+
}
3243

3344
return {
3445
realArgs: [Number(start), end, direction] as [
@@ -62,21 +73,45 @@ export function prepareSelectionInterceptor(
6273

6374
export function setUISelection(
6475
element: HTMLInputElement | HTMLTextAreaElement,
65-
start: number,
66-
end: number,
76+
{
77+
focusOffset: focusOffsetParam,
78+
anchorOffset: anchorOffsetParam = focusOffsetParam,
79+
}: {
80+
anchorOffset?: number
81+
focusOffset: number
82+
},
83+
mode: 'replace' | 'modify' = 'replace',
6784
) {
68-
element[UISelection] = {start, end}
85+
const valueLength = getUIValue(element).length
86+
const sanitizeOffset = (o: number) => Math.max(0, Math.min(valueLength, o))
87+
88+
const anchorOffset =
89+
mode === 'replace' || element[UISelection] === undefined
90+
? sanitizeOffset(anchorOffsetParam)
91+
: (element[UISelection] as UISelection).anchorOffset
92+
const focusOffset = sanitizeOffset(focusOffsetParam)
93+
94+
const startOffset = Math.min(anchorOffset, focusOffset)
95+
const endOffset = Math.max(anchorOffset, focusOffset)
6996

70-
if (element.selectionStart === start && element.selectionEnd === end) {
97+
element[UISelection] = {
98+
anchorOffset,
99+
focusOffset,
100+
}
101+
102+
if (
103+
element.selectionStart === startOffset &&
104+
element.selectionEnd === endOffset
105+
) {
71106
return
72107
}
73108

74109
// eslint-disable-next-line no-new-wrappers
75-
const startObj = new Number(start)
110+
const startObj = new Number(startOffset)
76111
;(startObj as Value)[UISelection] = UISelection
77112

78113
try {
79-
element.setSelectionRange(startObj as number, end)
114+
element.setSelectionRange(startObj as number, endOffset)
80115
} catch {
81116
// DOMException for invalid state is expected when calling this
82117
// on an element without support for setSelectionRange
@@ -86,16 +121,15 @@ export function setUISelection(
86121
export function getUISelection(
87122
element: HTMLInputElement | HTMLTextAreaElement,
88123
) {
89-
const ui = element[UISelection]
90-
return ui === undefined
91-
? {
92-
selectionStart: element.selectionStart,
93-
selectionEnd: element.selectionEnd,
94-
}
95-
: {
96-
selectionStart: ui.start,
97-
selectionEnd: ui.end,
98-
}
124+
const sel = element[UISelection] ?? {
125+
anchorOffset: element.selectionStart ?? 0,
126+
focusOffset: element.selectionEnd ?? 0,
127+
}
128+
return {
129+
...sel,
130+
startOffset: Math.min(sel.anchorOffset, sel.focusOffset),
131+
endOffset: Math.max(sel.anchorOffset, sel.focusOffset),
132+
}
99133
}
100134

101135
export function clearUISelection(

src/document/value.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ export function setUIValue(
5656
}
5757

5858
export function getUIValue(element: HTMLInputElement | HTMLTextAreaElement) {
59-
return element[UIValue] === undefined ? element.value : element[UIValue]
59+
return element[UIValue] === undefined
60+
? element.value
61+
: String(element[UIValue])
6062
}
6163

6264
export function setInitialValue(

src/keyboard/plugins/arrow.ts

+13-12
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
*/
55

66
import {behaviorPlugin} from '../types'
7-
import {getSelectionRange, isElementType, setSelectionRange} from '../../utils'
7+
import {isElementType, setSelection} from '../../utils'
8+
import {getUISelection} from '../../document'
89

910
export const keydownBehavior: behaviorPlugin[] = [
1011
{
@@ -13,18 +14,18 @@ export const keydownBehavior: behaviorPlugin[] = [
1314
(keyDef.key === 'ArrowLeft' || keyDef.key === 'ArrowRight') &&
1415
isElementType(element, ['input', 'textarea']),
1516
handle: (keyDef, element) => {
16-
const {selectionStart, selectionEnd} = getSelectionRange(element)
17+
const selection = getUISelection(element as HTMLInputElement)
1718

18-
const direction = keyDef.key === 'ArrowLeft' ? -1 : 1
19-
20-
const newPos =
21-
(selectionStart === selectionEnd
22-
? (selectionStart ?? /* istanbul ignore next */ 0) + direction
23-
: direction < 0
24-
? selectionStart
25-
: selectionEnd) ?? /* istanbul ignore next */ 0
26-
27-
setSelectionRange(element, newPos, newPos)
19+
// TODO: implement shift/ctrl
20+
setSelection({
21+
focusNode: element,
22+
focusOffset:
23+
selection.startOffset === selection.endOffset
24+
? selection.focusOffset + (keyDef.key === 'ArrowLeft' ? -1 : 1)
25+
: keyDef.key === 'ArrowLeft'
26+
? selection.startOffset
27+
: selection.endOffset,
28+
})
2829
},
2930
},
3031
]

src/keyboard/plugins/character.ts

+29-47
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@ import {
99
buildTimeValue,
1010
calculateNewValue,
1111
fireInputEvent,
12+
getInputRange,
1213
getSpaceUntilMaxLength,
1314
getValue,
14-
isClickableInput,
1515
isContentEditable,
16+
isEditableInput,
1617
isElementType,
1718
isValidDateValue,
1819
isValidInputTimeValue,
20+
prepareInput,
1921
} from '../../utils'
22+
import {UISelectionRange} from '../../document'
2023

2124
export const keypressBehavior: behaviorPlugin[] = [
2225
{
@@ -37,9 +40,10 @@ export const keypressBehavior: behaviorPlugin[] = [
3740
newEntry = timeNewEntry
3841
}
3942

40-
const {newValue, newSelectionStart} = calculateNewValue(
43+
const {newValue, newOffset} = calculateNewValue(
4144
newEntry,
42-
element as HTMLElement,
45+
element as HTMLInputElement & {type: 'time'},
46+
getInputRange(element) as UISelectionRange,
4347
)
4448
const prevValue = getValue(element)
4549

@@ -48,7 +52,10 @@ export const keypressBehavior: behaviorPlugin[] = [
4852
if (prevValue !== newValue) {
4953
fireInputEvent(element as HTMLInputElement, {
5054
newValue,
51-
newSelectionStart,
55+
newSelection: {
56+
node: element,
57+
offset: newOffset,
58+
},
5259
eventOverrides: {
5360
data: keyDef.key,
5461
inputType: 'insertText',
@@ -81,9 +88,10 @@ export const keypressBehavior: behaviorPlugin[] = [
8188
newEntry = textToBeTyped
8289
}
8390

84-
const {newValue, newSelectionStart} = calculateNewValue(
91+
const {newValue, newOffset} = calculateNewValue(
8592
newEntry,
86-
element as HTMLElement,
93+
element as HTMLInputElement & {type: 'date'},
94+
getInputRange(element) as UISelectionRange,
8795
)
8896
const prevValue = getValue(element)
8997

@@ -92,7 +100,10 @@ export const keypressBehavior: behaviorPlugin[] = [
92100
if (prevValue !== newValue) {
93101
fireInputEvent(element as HTMLInputElement, {
94102
newValue,
95-
newSelectionStart,
103+
newSelection: {
104+
node: element,
105+
offset: newOffset,
106+
},
96107
eventOverrides: {
97108
data: keyDef.key,
98109
inputType: 'insertText',
@@ -118,10 +129,10 @@ export const keypressBehavior: behaviorPlugin[] = [
118129
return
119130
}
120131

121-
const {newValue, newSelectionStart} = calculateNewValue(
132+
const {newValue, commit} = prepareInput(
122133
keyDef.key as string,
123-
element as HTMLElement,
124-
)
134+
element,
135+
) as NonNullable<ReturnType<typeof prepareInput>>
125136

126137
// the browser allows some invalid input but not others
127138
// it allows up to two '-' at any place before any 'e' or one directly following 'e'
@@ -135,37 +146,18 @@ export const keypressBehavior: behaviorPlugin[] = [
135146
return
136147
}
137148

138-
fireInputEvent(element as HTMLInputElement, {
139-
newValue,
140-
newSelectionStart,
141-
eventOverrides: {
142-
data: keyDef.key,
143-
inputType: 'insertText',
144-
},
145-
})
149+
commit()
146150
},
147151
},
148152
{
149153
matches: (keyDef, element) =>
150154
keyDef.key?.length === 1 &&
151-
((isElementType(element, ['input', 'textarea'], {readOnly: false}) &&
152-
!isClickableInput(element)) ||
155+
(isEditableInput(element) ||
156+
isElementType(element, 'textarea', {readOnly: false}) ||
153157
isContentEditable(element)) &&
154158
getSpaceUntilMaxLength(element) !== 0,
155159
handle: (keyDef, element) => {
156-
const {newValue, newSelectionStart} = calculateNewValue(
157-
keyDef.key as string,
158-
element as HTMLElement,
159-
)
160-
161-
fireInputEvent(element as HTMLElement, {
162-
newValue,
163-
newSelectionStart,
164-
eventOverrides: {
165-
data: keyDef.key,
166-
inputType: 'insertText',
167-
},
168-
})
160+
prepareInput(keyDef.key as string, element)?.commit()
169161
},
170162
},
171163
{
@@ -175,23 +167,13 @@ export const keypressBehavior: behaviorPlugin[] = [
175167
isContentEditable(element)) &&
176168
getSpaceUntilMaxLength(element) !== 0,
177169
handle: (keyDef, element, options, state) => {
178-
const {newValue, newSelectionStart} = calculateNewValue(
170+
prepareInput(
179171
'\n',
180-
element as HTMLElement,
181-
)
182-
183-
const inputType =
172+
element,
184173
isContentEditable(element) && !state.modifiers.shift
185174
? 'insertParagraph'
186-
: 'insertLineBreak'
187-
188-
fireInputEvent(element as HTMLElement, {
189-
newValue,
190-
newSelectionStart,
191-
eventOverrides: {
192-
inputType,
193-
},
194-
})
175+
: 'insertLineBreak',
176+
)?.commit()
195177
},
196178
},
197179
]

src/keyboard/plugins/control.ts

+3-19
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,11 @@
55

66
import {behaviorPlugin} from '../types'
77
import {
8-
calculateNewValue,
9-
fireInputEvent,
108
getValue,
119
isContentEditable,
12-
isCursorAtEnd,
1310
isEditable,
1411
isElementType,
12+
prepareInput,
1513
setSelectionRange,
1614
} from '../../utils'
1715

@@ -47,23 +45,9 @@ export const keydownBehavior: behaviorPlugin[] = [
4745
},
4846
{
4947
matches: (keyDef, element) =>
50-
keyDef.key === 'Delete' && isEditable(element) && !isCursorAtEnd(element),
48+
keyDef.key === 'Delete' && isEditable(element),
5149
handle: (keDef, element) => {
52-
const {newValue, newSelectionStart} = calculateNewValue(
53-
'',
54-
element as HTMLElement,
55-
undefined,
56-
undefined,
57-
'forward',
58-
)
59-
60-
fireInputEvent(element as HTMLElement, {
61-
newValue,
62-
newSelectionStart,
63-
eventOverrides: {
64-
inputType: 'deleteContentForward',
65-
},
66-
})
50+
prepareInput('', element, 'deleteContentForward')?.commit()
6751
},
6852
},
6953
]

0 commit comments

Comments
 (0)