@flighthq/textinput 0.2.0 → 0.2.1-next.423.3350e53
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/package.json +6 -6
- package/src/textInputEditing.test.ts +70 -0
- package/src/textInputManager.test.ts +101 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/textinput",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1-next.423.3350e53",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/flighthq/flight.git",
|
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@flighthq/node": "0.2.
|
|
36
|
-
"@flighthq/signals": "0.2.
|
|
37
|
-
"@flighthq/text": "0.2.
|
|
38
|
-
"@flighthq/textlayout": "0.2.
|
|
39
|
-
"@flighthq/types": "0.2.
|
|
35
|
+
"@flighthq/node": "0.2.1-next.423.3350e53",
|
|
36
|
+
"@flighthq/signals": "0.2.1-next.423.3350e53",
|
|
37
|
+
"@flighthq/text": "0.2.1-next.423.3350e53",
|
|
38
|
+
"@flighthq/textlayout": "0.2.1-next.423.3350e53",
|
|
39
|
+
"@flighthq/types": "0.2.1-next.423.3350e53"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"typescript": "^5.3.0"
|
|
@@ -839,6 +839,34 @@ describe('selectLineAtTextInputIndex', () => {
|
|
|
839
839
|
expect(getTextInputSelectionBeginIndex(text)).toBe(6);
|
|
840
840
|
expect(getTextInputSelectionEndIndex(text)).toBe(11);
|
|
841
841
|
});
|
|
842
|
+
|
|
843
|
+
it('selects the correct line in multiline text', () => {
|
|
844
|
+
const text = createInput({ text: 'aaa\nbbb\nccc' });
|
|
845
|
+
selectLineAtTextInputIndex(text, 5);
|
|
846
|
+
expect(getTextInputSelectionBeginIndex(text)).toBe(4);
|
|
847
|
+
expect(getTextInputSelectionEndIndex(text)).toBe(7);
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
it('selects the first line when index is at a line boundary', () => {
|
|
851
|
+
const text = createInput({ text: 'hello\nworld' });
|
|
852
|
+
selectLineAtTextInputIndex(text, 5);
|
|
853
|
+
expect(getTextInputSelectionBeginIndex(text)).toBe(0);
|
|
854
|
+
expect(getTextInputSelectionEndIndex(text)).toBe(5);
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
it('selects empty range for empty text', () => {
|
|
858
|
+
const text = createInput({ text: '' });
|
|
859
|
+
selectLineAtTextInputIndex(text, 0);
|
|
860
|
+
expect(getTextInputSelectionBeginIndex(text)).toBe(0);
|
|
861
|
+
expect(getTextInputSelectionEndIndex(text)).toBe(0);
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
it('selects the last line', () => {
|
|
865
|
+
const text = createInput({ text: 'aaa\nbbb\nccc' });
|
|
866
|
+
selectLineAtTextInputIndex(text, 10);
|
|
867
|
+
expect(getTextInputSelectionBeginIndex(text)).toBe(8);
|
|
868
|
+
expect(getTextInputSelectionEndIndex(text)).toBe(11);
|
|
869
|
+
});
|
|
842
870
|
});
|
|
843
871
|
|
|
844
872
|
describe('selectWordAtTextInputIndex', () => {
|
|
@@ -848,6 +876,48 @@ describe('selectWordAtTextInputIndex', () => {
|
|
|
848
876
|
expect(getTextInputSelectionBeginIndex(text)).toBe(0);
|
|
849
877
|
expect(getTextInputSelectionEndIndex(text)).toBe(5);
|
|
850
878
|
});
|
|
879
|
+
|
|
880
|
+
it('selects the preceding word when index is at a whitespace boundary', () => {
|
|
881
|
+
const text = createInput({ text: 'hello world' });
|
|
882
|
+
selectWordAtTextInputIndex(text, 5);
|
|
883
|
+
expect(getTextInputSelectionBeginIndex(text)).toBe(0);
|
|
884
|
+
expect(getTextInputSelectionEndIndex(text)).toBe(5);
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
it('selects non-word region when surrounded by non-word characters', () => {
|
|
888
|
+
const text = createInput({ text: ' hello' });
|
|
889
|
+
selectWordAtTextInputIndex(text, 1);
|
|
890
|
+
expect(getTextInputSelectionBeginIndex(text)).toBe(0);
|
|
891
|
+
expect(getTextInputSelectionEndIndex(text)).toBe(2);
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
it('selects the preceding word when index is at punctuation', () => {
|
|
895
|
+
const text = createInput({ text: 'hello, world' });
|
|
896
|
+
selectWordAtTextInputIndex(text, 5);
|
|
897
|
+
expect(getTextInputSelectionBeginIndex(text)).toBe(0);
|
|
898
|
+
expect(getTextInputSelectionEndIndex(text)).toBe(5);
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
it('selects word at start of text (index 0)', () => {
|
|
902
|
+
const text = createInput({ text: 'hello world' });
|
|
903
|
+
selectWordAtTextInputIndex(text, 0);
|
|
904
|
+
expect(getTextInputSelectionBeginIndex(text)).toBe(0);
|
|
905
|
+
expect(getTextInputSelectionEndIndex(text)).toBe(5);
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
it('selects word at end of text', () => {
|
|
909
|
+
const text = createInput({ text: 'hello world' });
|
|
910
|
+
selectWordAtTextInputIndex(text, 11);
|
|
911
|
+
expect(getTextInputSelectionBeginIndex(text)).toBe(6);
|
|
912
|
+
expect(getTextInputSelectionEndIndex(text)).toBe(11);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
it('selects empty range for empty text', () => {
|
|
916
|
+
const text = createInput({ text: '' });
|
|
917
|
+
selectWordAtTextInputIndex(text, 0);
|
|
918
|
+
expect(getTextInputSelectionBeginIndex(text)).toBe(0);
|
|
919
|
+
expect(getTextInputSelectionEndIndex(text)).toBe(0);
|
|
920
|
+
});
|
|
851
921
|
});
|
|
852
922
|
|
|
853
923
|
describe('setTextInputSelection', () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createSignal, emitSignal } from '@flighthq/signals';
|
|
2
|
-
import { createRichText } from '@flighthq/text';
|
|
3
|
-
import type { RichText, RichTextData } from '@flighthq/types';
|
|
2
|
+
import { createRichText, getRichTextRuntime } from '@flighthq/text';
|
|
3
|
+
import type { RichText, RichTextData, RichTextRuntime, TextLayoutResult } from '@flighthq/types';
|
|
4
4
|
import { KeyCode } from '@flighthq/types';
|
|
5
5
|
|
|
6
6
|
import { enableTextInput, getTextInputState } from './textInput';
|
|
@@ -27,6 +27,42 @@ function createInput(data: Partial<RichTextData> = {}): RichText {
|
|
|
27
27
|
return text;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// Creates a mock layout for "hello world" (11 chars) as a single line. Each character is 10px wide,
|
|
31
|
+
// starting at offsetX=2. This makes getTextInputCharacterIndexAtPoint predictable: x=7 lands on
|
|
32
|
+
// index 0, x=17 on index 1, etc. (midpoint rounding: x < offsetX + advance/2 → that index).
|
|
33
|
+
function createSingleLineLayout(): TextLayoutResult {
|
|
34
|
+
return {
|
|
35
|
+
groups: [
|
|
36
|
+
{
|
|
37
|
+
ascent: 10,
|
|
38
|
+
descent: 2,
|
|
39
|
+
endIndex: 11,
|
|
40
|
+
format: {},
|
|
41
|
+
height: 12,
|
|
42
|
+
leading: 0,
|
|
43
|
+
lineIndex: 0,
|
|
44
|
+
offsetX: 2,
|
|
45
|
+
offsetY: 2,
|
|
46
|
+
positions: [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10],
|
|
47
|
+
startIndex: 0,
|
|
48
|
+
width: 110,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
lineAscents: [10],
|
|
52
|
+
lineDescents: [2],
|
|
53
|
+
lineHeights: [12],
|
|
54
|
+
lineLeadings: [0],
|
|
55
|
+
lineWidths: [110],
|
|
56
|
+
numLines: 1,
|
|
57
|
+
textHeight: 12,
|
|
58
|
+
textWidth: 110,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function setLayout(target: RichText, layout: TextLayoutResult): void {
|
|
63
|
+
(getRichTextRuntime(target) as RichTextRuntime).textLayout = layout;
|
|
64
|
+
}
|
|
65
|
+
|
|
30
66
|
function isFocused(node: RichText): boolean {
|
|
31
67
|
return getTextInputState(node)?.focused === true;
|
|
32
68
|
}
|
|
@@ -129,12 +165,75 @@ describe('dispatchTextInputPointerDown', () => {
|
|
|
129
165
|
expect(manager.focused).toBe(target);
|
|
130
166
|
expect(isFocused(target)).toBe(true);
|
|
131
167
|
});
|
|
168
|
+
|
|
169
|
+
it('selects a word on double-click (clickCount=2)', () => {
|
|
170
|
+
const manager = createTextInputManager();
|
|
171
|
+
const target = createInput({ text: 'hello world' });
|
|
172
|
+
setLayout(target, createSingleLineLayout());
|
|
173
|
+
// x=7 lands on index 0 (inside "hello"); clickCount=2 selects the word.
|
|
174
|
+
dispatchTextInputPointerDown(manager, target, 7, 5, false, 2);
|
|
175
|
+
expect(getTextInputSelectionBeginIndex(target)).toBe(0);
|
|
176
|
+
expect(getTextInputSelectionEndIndex(target)).toBe(5);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('selects a line on triple-click (clickCount=3)', () => {
|
|
180
|
+
const manager = createTextInputManager();
|
|
181
|
+
const target = createInput({ text: 'hello world' });
|
|
182
|
+
setLayout(target, createSingleLineLayout());
|
|
183
|
+
// Any x on the single line; clickCount=3 selects the entire line.
|
|
184
|
+
dispatchTextInputPointerDown(manager, target, 7, 5, false, 3);
|
|
185
|
+
expect(getTextInputSelectionBeginIndex(target)).toBe(0);
|
|
186
|
+
expect(getTextInputSelectionEndIndex(target)).toBe(11);
|
|
187
|
+
});
|
|
132
188
|
});
|
|
133
189
|
|
|
134
190
|
describe('dispatchTextInputPointerMove', () => {
|
|
135
191
|
it('does not throw when nothing is focused', () => {
|
|
136
192
|
expect(() => dispatchTextInputPointerMove(createTextInputManager(), 0, 0)).not.toThrow();
|
|
137
193
|
});
|
|
194
|
+
|
|
195
|
+
it('extends selection when dragging after pointer-down', () => {
|
|
196
|
+
const manager = createTextInputManager();
|
|
197
|
+
const target = createInput({ text: 'hello world' });
|
|
198
|
+
const layout = createSingleLineLayout();
|
|
199
|
+
setLayout(target, layout);
|
|
200
|
+
// Pointer-down at index 2 (x=2+2*10=22, midpoint at 27; use 23 to land on index 2).
|
|
201
|
+
dispatchTextInputPointerDown(manager, target, 23, 5);
|
|
202
|
+
expect(getTextInputSelectionBeginIndex(target)).toBe(2);
|
|
203
|
+
expect(getTextInputSelectionEndIndex(target)).toBe(2);
|
|
204
|
+
// Pointer-move to index 7 (x=2+7*10=72, midpoint at 77; use 73 to land on index 7).
|
|
205
|
+
dispatchTextInputPointerMove(manager, 73, 5);
|
|
206
|
+
expect(getTextInputSelectionBeginIndex(target)).toBe(2);
|
|
207
|
+
expect(getTextInputSelectionEndIndex(target)).toBe(7);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('extends selection backward when pointer moves left', () => {
|
|
211
|
+
const manager = createTextInputManager();
|
|
212
|
+
const target = createInput({ text: 'hello world' });
|
|
213
|
+
const layout = createSingleLineLayout();
|
|
214
|
+
setLayout(target, layout);
|
|
215
|
+
// Pointer-down at index 7 (x=73).
|
|
216
|
+
dispatchTextInputPointerDown(manager, target, 73, 5);
|
|
217
|
+
// Pointer-move to index 2 (x=23).
|
|
218
|
+
dispatchTextInputPointerMove(manager, 23, 5);
|
|
219
|
+
expect(getTextInputSelectionBeginIndex(target)).toBe(2);
|
|
220
|
+
expect(getTextInputSelectionEndIndex(target)).toBe(7);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('clamps selection at text boundaries', () => {
|
|
224
|
+
const manager = createTextInputManager();
|
|
225
|
+
const target = createInput({ text: 'hello world' });
|
|
226
|
+
const layout = createSingleLineLayout();
|
|
227
|
+
setLayout(target, layout);
|
|
228
|
+
// Pointer-down at index 5.
|
|
229
|
+
dispatchTextInputPointerDown(manager, target, 53, 5);
|
|
230
|
+
// Pointer-move far past the end of text.
|
|
231
|
+
dispatchTextInputPointerMove(manager, 999, 5);
|
|
232
|
+
expect(getTextInputSelectionEndIndex(target)).toBe(11);
|
|
233
|
+
// Pointer-move far before the start of text.
|
|
234
|
+
dispatchTextInputPointerMove(manager, -100, 5);
|
|
235
|
+
expect(getTextInputSelectionBeginIndex(target)).toBe(0);
|
|
236
|
+
});
|
|
138
237
|
});
|
|
139
238
|
|
|
140
239
|
describe('dispatchTextInputWheel', () => {
|