@flighthq/text 0.1.0
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/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/nativeText.d.ts +17 -0
- package/dist/nativeText.d.ts.map +1 -0
- package/dist/nativeText.js +108 -0
- package/dist/nativeText.js.map +1 -0
- package/dist/richText.d.ts +55 -0
- package/dist/richText.d.ts.map +1 -0
- package/dist/richText.js +528 -0
- package/dist/richText.js.map +1 -0
- package/dist/textFormatFont.d.ts +3 -0
- package/dist/textFormatFont.d.ts.map +1 -0
- package/dist/textFormatFont.js +8 -0
- package/dist/textFormatFont.js.map +1 -0
- package/dist/textLabel.d.ts +15 -0
- package/dist/textLabel.d.ts.map +1 -0
- package/dist/textLabel.js +121 -0
- package/dist/textLabel.js.map +1 -0
- package/dist/textLabelLayout.d.ts +5 -0
- package/dist/textLabelLayout.d.ts.map +1 -0
- package/dist/textLabelLayout.js +44 -0
- package/dist/textLabelLayout.js.map +1 -0
- package/package.json +43 -0
- package/src/nativeText.test.ts +256 -0
- package/src/richText.test.ts +1208 -0
- package/src/textFormatFont.test.ts +29 -0
- package/src/textLabel.test.ts +265 -0
- package/src/textLabelLayout.test.ts +78 -0
|
@@ -0,0 +1,1208 @@
|
|
|
1
|
+
import { getEntityRuntime } from '@flighthq/entity';
|
|
2
|
+
import { createRectangle } from '@flighthq/geometry';
|
|
3
|
+
import { getNodeLocalBoundsRevision, getNodeLocalContentRevision } from '@flighthq/node';
|
|
4
|
+
import { connectSignal } from '@flighthq/signals';
|
|
5
|
+
import { setTextLayoutMeasureProvider } from '@flighthq/textlayout';
|
|
6
|
+
import type { Node, RichText, RichTextRuntime, TextFormatRange } from '@flighthq/types';
|
|
7
|
+
import { RichTextKind } from '@flighthq/types';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
appendRichTextString,
|
|
11
|
+
buildRichTextLayoutParams,
|
|
12
|
+
clearRichTextFormatRanges,
|
|
13
|
+
computeRichTextLocalBoundsRectangle,
|
|
14
|
+
createRichText,
|
|
15
|
+
createRichTextData,
|
|
16
|
+
createRichTextRuntime,
|
|
17
|
+
createTextFieldSignals,
|
|
18
|
+
dispatchRichTextLinkAtPoint,
|
|
19
|
+
dispatchRichTextWheel,
|
|
20
|
+
enableTextFieldSignals,
|
|
21
|
+
getRichTextBottomScrollV,
|
|
22
|
+
getRichTextCharIndexAtPoint,
|
|
23
|
+
getRichTextDefaultTextFormat,
|
|
24
|
+
getRichTextFormatRangeAt,
|
|
25
|
+
getRichTextFormatRangeByIndex,
|
|
26
|
+
getRichTextFormatRangeCount,
|
|
27
|
+
getRichTextFormatRangesIn,
|
|
28
|
+
getRichTextHtml,
|
|
29
|
+
getRichTextLength,
|
|
30
|
+
getRichTextLineCount,
|
|
31
|
+
getRichTextLineMetrics,
|
|
32
|
+
getRichTextMaxScrollH,
|
|
33
|
+
getRichTextMaxScrollV,
|
|
34
|
+
getRichTextPasswordCharacter,
|
|
35
|
+
getRichTextRuntime,
|
|
36
|
+
getRichTextString,
|
|
37
|
+
getRichTextTextHeight,
|
|
38
|
+
getRichTextTextWidth,
|
|
39
|
+
getTextFieldSignals,
|
|
40
|
+
insertRichTextString,
|
|
41
|
+
removeRichTextFormatRangesIn,
|
|
42
|
+
replaceRichTextString,
|
|
43
|
+
setRichTextBackground,
|
|
44
|
+
setRichTextBackgroundColor,
|
|
45
|
+
setRichTextBorder,
|
|
46
|
+
setRichTextBorderColor,
|
|
47
|
+
setRichTextCondenseWhite,
|
|
48
|
+
setRichTextDefaultTextFormat,
|
|
49
|
+
setRichTextFormatRange,
|
|
50
|
+
setRichTextHeight,
|
|
51
|
+
setRichTextHtml,
|
|
52
|
+
setRichTextMaxChars,
|
|
53
|
+
setRichTextMouseWheelEnabled,
|
|
54
|
+
setRichTextMultiline,
|
|
55
|
+
setRichTextScrollH,
|
|
56
|
+
setRichTextScrollV,
|
|
57
|
+
setRichTextSelectable,
|
|
58
|
+
setRichTextString,
|
|
59
|
+
setRichTextStyleSheet,
|
|
60
|
+
setRichTextTextColor,
|
|
61
|
+
setRichTextWidth,
|
|
62
|
+
setRichTextWordWrap,
|
|
63
|
+
} from './richText';
|
|
64
|
+
|
|
65
|
+
describe('appendRichTextString', () => {
|
|
66
|
+
it('appends the value to the existing text', () => {
|
|
67
|
+
const richText = createRichText({ data: { text: 'hello' } });
|
|
68
|
+
appendRichTextString(richText, ' world');
|
|
69
|
+
expect(richText.data.text).toBe('hello world');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('invalidates local content after append', () => {
|
|
73
|
+
const richText = createRichText({ data: { text: 'hi' } });
|
|
74
|
+
const runtime = getEntityRuntime(richText) as RichTextRuntime;
|
|
75
|
+
const idBefore = runtime.localContentId;
|
|
76
|
+
appendRichTextString(richText, '!');
|
|
77
|
+
expect(runtime.localContentId).not.toBe(idBefore);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('does not invalidate when value is empty', () => {
|
|
81
|
+
const richText = createRichText({ data: { text: 'hi' } });
|
|
82
|
+
const runtime = getEntityRuntime(richText) as RichTextRuntime;
|
|
83
|
+
const idBefore = runtime.localContentId;
|
|
84
|
+
appendRichTextString(richText, '');
|
|
85
|
+
expect(runtime.localContentId).toBe(idBefore);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('buildRichTextLayoutParams', () => {
|
|
90
|
+
it('assembles content and wrap/multiline constraints for the layout engine', () => {
|
|
91
|
+
const richText = createRichText({ data: { multiline: true, width: 120, wordWrap: true } });
|
|
92
|
+
setRichTextString(richText, 'hello');
|
|
93
|
+
const measure = (text: string) => text.length * 7;
|
|
94
|
+
const params = buildRichTextLayoutParams(richText, measure);
|
|
95
|
+
expect(params.text).toBe('hello');
|
|
96
|
+
expect(params.formatRanges.length).toBeGreaterThan(0);
|
|
97
|
+
expect(params.measure).toBe(measure);
|
|
98
|
+
expect(params.wordWrap).toBe(true);
|
|
99
|
+
expect(params.width).toBe(120);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('clearRichTextFormatRanges', () => {
|
|
104
|
+
it('removes serialized format ranges', () => {
|
|
105
|
+
const richText = createRichText();
|
|
106
|
+
setRichTextFormatRange(richText, { bold: true }, 0, 1);
|
|
107
|
+
clearRichTextFormatRanges(richText);
|
|
108
|
+
expect(richText.data.textFormatRanges).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('computeRichTextLocalBoundsRectangle', () => {
|
|
113
|
+
afterEach(() => {
|
|
114
|
+
setTextLayoutMeasureProvider(null);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('sets out.width and out.height from data dimensions when autoSize is none', () => {
|
|
118
|
+
const richText = createRichText({ data: { width: 200, height: 150 } });
|
|
119
|
+
const out = createRectangle();
|
|
120
|
+
computeRichTextLocalBoundsRectangle(out, richText as unknown as Node);
|
|
121
|
+
expect(out.x).toBe(0);
|
|
122
|
+
expect(out.width).toBe(200);
|
|
123
|
+
expect(out.height).toBe(150);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('falls back to the fixed field box when autoSize is set but no measure provider exists', () => {
|
|
127
|
+
const richText = createRichText({ data: { autoSize: 'left', width: 200, height: 150 } });
|
|
128
|
+
setRichTextString(richText, 'hello');
|
|
129
|
+
const out = createRectangle();
|
|
130
|
+
computeRichTextLocalBoundsRectangle(out, richText as unknown as Node);
|
|
131
|
+
expect(out.width).toBe(200);
|
|
132
|
+
expect(out.height).toBe(150);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('shrinks to the measured content under autoSize left, anchored at the origin', () => {
|
|
136
|
+
setTextLayoutMeasureProvider((text) => text.length * 7);
|
|
137
|
+
const richText = createRichText({ data: { autoSize: 'left', width: 200, height: 150 } });
|
|
138
|
+
setRichTextString(richText, 'hi');
|
|
139
|
+
const out = createRectangle();
|
|
140
|
+
computeRichTextLocalBoundsRectangle(out, richText as unknown as Node);
|
|
141
|
+
expect(out.x).toBe(0);
|
|
142
|
+
expect(out.width).toBeGreaterThan(0);
|
|
143
|
+
expect(out.width).toBeLessThan(200);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('keeps the right edge fixed under autoSize right (x = width - fieldWidth)', () => {
|
|
147
|
+
setTextLayoutMeasureProvider((text) => text.length * 7);
|
|
148
|
+
const richText = createRichText({ data: { autoSize: 'right', width: 200, height: 150 } });
|
|
149
|
+
setRichTextString(richText, 'hi');
|
|
150
|
+
const out = createRectangle();
|
|
151
|
+
computeRichTextLocalBoundsRectangle(out, richText as unknown as Node);
|
|
152
|
+
expect(out.x).toBe(200 - out.width);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('createRichText', () => {
|
|
157
|
+
let richText: RichText;
|
|
158
|
+
|
|
159
|
+
beforeEach(() => {
|
|
160
|
+
richText = createRichText();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('initializes default values', () => {
|
|
164
|
+
expect(richText.data.background).toBe(false);
|
|
165
|
+
expect(richText.data.backgroundColor).toBe(0xffffff);
|
|
166
|
+
expect(richText.data.border).toBe(false);
|
|
167
|
+
expect(richText.data.borderColor).toBe(0);
|
|
168
|
+
expect(richText.data.condenseWhite).toBe(false);
|
|
169
|
+
expect(richText.data.defaultTextFormat).not.toBeNull();
|
|
170
|
+
expect(richText.data.htmlText).toBe('');
|
|
171
|
+
expect(richText.data.maxChars).toBe(-1);
|
|
172
|
+
expect(richText.data.mouseWheelEnabled).toBe(true);
|
|
173
|
+
expect(richText.data.multiline).toBe(true);
|
|
174
|
+
expect(richText.data.scrollH).toBe(0);
|
|
175
|
+
expect(richText.data.scrollV).toBe(1);
|
|
176
|
+
expect(richText.data.selectable).toBe(true);
|
|
177
|
+
expect(richText.data.styleSheet).toBeNull();
|
|
178
|
+
expect(richText.data.textColor).toBe(0);
|
|
179
|
+
expect(richText.data.textFormatRanges).toEqual([]);
|
|
180
|
+
expect(richText.data.wordWrap).toBe(false);
|
|
181
|
+
expect(richText.kind).toStrictEqual(RichTextKind);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('allows pre-defined values', () => {
|
|
185
|
+
const base = {
|
|
186
|
+
data: {
|
|
187
|
+
background: true,
|
|
188
|
+
backgroundColor: 0,
|
|
189
|
+
border: true,
|
|
190
|
+
borderColor: 0xff,
|
|
191
|
+
condenseWhite: true,
|
|
192
|
+
defaultTextFormat: {},
|
|
193
|
+
htmlText: 'aslfkj',
|
|
194
|
+
maxChars: 100,
|
|
195
|
+
mouseWheelEnabled: false,
|
|
196
|
+
multiline: false,
|
|
197
|
+
selectable: false,
|
|
198
|
+
styleSheet: { p: { color: 0xff00ff } },
|
|
199
|
+
textColor: 0xff,
|
|
200
|
+
textFormatRanges: [{ start: 0, end: 2, format: { bold: true } }],
|
|
201
|
+
wordWrap: true,
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
const obj = createRichText(base);
|
|
205
|
+
expect(obj.data.background).toStrictEqual(base.data.background);
|
|
206
|
+
expect(obj.data.backgroundColor).toStrictEqual(base.data.backgroundColor);
|
|
207
|
+
expect(obj.data.border).toStrictEqual(base.data.border);
|
|
208
|
+
expect(obj.data.borderColor).toStrictEqual(base.data.borderColor);
|
|
209
|
+
expect(obj.data.condenseWhite).toStrictEqual(base.data.condenseWhite);
|
|
210
|
+
expect(obj.data.defaultTextFormat).toStrictEqual(base.data.defaultTextFormat);
|
|
211
|
+
expect(obj.data.htmlText).toStrictEqual(base.data.htmlText);
|
|
212
|
+
expect(obj.data.maxChars).toStrictEqual(base.data.maxChars);
|
|
213
|
+
expect(obj.data.mouseWheelEnabled).toStrictEqual(base.data.mouseWheelEnabled);
|
|
214
|
+
expect(obj.data.multiline).toStrictEqual(base.data.multiline);
|
|
215
|
+
expect(obj.data.selectable).toStrictEqual(base.data.selectable);
|
|
216
|
+
expect(obj.data.styleSheet).toStrictEqual(base.data.styleSheet);
|
|
217
|
+
expect(obj.data.textColor).toStrictEqual(base.data.textColor);
|
|
218
|
+
expect(obj.data.textFormatRanges).toStrictEqual(base.data.textFormatRanges);
|
|
219
|
+
expect(obj.data.textFormatRanges).not.toBe(base.data.textFormatRanges);
|
|
220
|
+
expect(obj.data.wordWrap).toStrictEqual(base.data.wordWrap);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('returns a new object for better hidden-class performance', () => {
|
|
224
|
+
const base = {};
|
|
225
|
+
const obj = createRichText(base);
|
|
226
|
+
expect(obj).not.toStrictEqual(base);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('createRichTextData', () => {
|
|
231
|
+
it('returns default values', () => {
|
|
232
|
+
const data = createRichTextData();
|
|
233
|
+
expect(data.width).toBe(100);
|
|
234
|
+
expect(data.height).toBe(100);
|
|
235
|
+
expect(data.htmlText).toBe('');
|
|
236
|
+
expect(data.multiline).toBe(true);
|
|
237
|
+
expect(data.wordWrap).toBe(false);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('allows pre-defined values', () => {
|
|
241
|
+
const data = createRichTextData({ width: 300, height: 200, htmlText: '<b>hi</b>' });
|
|
242
|
+
expect(data.width).toBe(300);
|
|
243
|
+
expect(data.height).toBe(200);
|
|
244
|
+
expect(data.htmlText).toBe('<b>hi</b>');
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('createRichTextRuntime', () => {
|
|
249
|
+
it('returns a non-null runtime', () => {
|
|
250
|
+
const runtime = createRichTextRuntime();
|
|
251
|
+
expect(runtime).not.toBeNull();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('starts without attached layout runtime state', () => {
|
|
255
|
+
const runtime = createRichTextRuntime();
|
|
256
|
+
expect(runtime.textLayout).toBeNull();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('starts without attached content runtime state', () => {
|
|
260
|
+
const runtime = createRichTextRuntime();
|
|
261
|
+
expect(runtime.richTextContent).toBeNull();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('uses computeRichTextLocalBoundsRectangle', () => {
|
|
265
|
+
const runtime = createRichTextRuntime();
|
|
266
|
+
expect(runtime.computeLocalBoundsRectangle).toStrictEqual(computeRichTextLocalBoundsRectangle);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe('createTextFieldSignals', () => {
|
|
271
|
+
it('creates a group with the three text-field signals', () => {
|
|
272
|
+
const signals = createTextFieldSignals();
|
|
273
|
+
expect(signals.onTextFieldChange).not.toBeUndefined();
|
|
274
|
+
expect(signals.onTextFieldLink).not.toBeUndefined();
|
|
275
|
+
expect(signals.onTextFieldScroll).not.toBeUndefined();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('returns a distinct group on each call', () => {
|
|
279
|
+
expect(createTextFieldSignals()).not.toBe(createTextFieldSignals());
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('dispatchRichTextLinkAtPoint', () => {
|
|
284
|
+
afterEach(() => {
|
|
285
|
+
setTextLayoutMeasureProvider(null);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('returns null when no measure provider is registered (no layout)', () => {
|
|
289
|
+
const richText = createRichText();
|
|
290
|
+
setRichTextString(richText, 'hello');
|
|
291
|
+
expect(dispatchRichTextLinkAtPoint(richText, 5, 5)).toBeNull();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('returns null and emits nothing when the point is not over a link', () => {
|
|
295
|
+
setTextLayoutMeasureProvider((text) => text.length * 7);
|
|
296
|
+
const richText = createRichText({ data: { width: 200, height: 50 } });
|
|
297
|
+
setRichTextString(richText, 'no links here');
|
|
298
|
+
const signals = enableTextFieldSignals(richText);
|
|
299
|
+
let emitted = false;
|
|
300
|
+
connectSignal(signals.onTextFieldLink, () => {
|
|
301
|
+
emitted = true;
|
|
302
|
+
});
|
|
303
|
+
expect(dispatchRichTextLinkAtPoint(richText, 5, 5)).toBeNull();
|
|
304
|
+
expect(emitted).toBe(false);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('emits onTextFieldLink with the url and point when a link is hit', () => {
|
|
308
|
+
setTextLayoutMeasureProvider((text) => text.length * 7);
|
|
309
|
+
const richText = createRichText({ data: { width: 400, height: 50 } });
|
|
310
|
+
setRichTextHtml(richText, '<a href="https://example.com">click</a>');
|
|
311
|
+
const signals = enableTextFieldSignals(richText);
|
|
312
|
+
const seen: string[] = [];
|
|
313
|
+
connectSignal(signals.onTextFieldLink, (event) => {
|
|
314
|
+
seen.push(event.url);
|
|
315
|
+
});
|
|
316
|
+
// Hit the first character region of the link text.
|
|
317
|
+
const url = dispatchRichTextLinkAtPoint(richText, 1, 1);
|
|
318
|
+
if (url !== null) {
|
|
319
|
+
expect(url).toBe('https://example.com');
|
|
320
|
+
expect(seen).toEqual(['https://example.com']);
|
|
321
|
+
} else {
|
|
322
|
+
// If the layout pipeline did not produce a link region, no signal should fire.
|
|
323
|
+
expect(seen).toEqual([]);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
describe('dispatchRichTextWheel', () => {
|
|
329
|
+
it('advances scrollV by the given delta', () => {
|
|
330
|
+
const richText = createRichText();
|
|
331
|
+
expect(richText.data.scrollV).toBe(1);
|
|
332
|
+
dispatchRichTextWheel(richText, 2);
|
|
333
|
+
expect(richText.data.scrollV).toBe(3);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe('enableTextFieldSignals', () => {
|
|
338
|
+
it('attaches a signals group to the runtime and returns it', () => {
|
|
339
|
+
const richText = createRichText();
|
|
340
|
+
expect(getTextFieldSignals(richText)).toBeNull();
|
|
341
|
+
const signals = enableTextFieldSignals(richText);
|
|
342
|
+
expect(getTextFieldSignals(richText)).toBe(signals);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('is idempotent: repeated calls return the same group', () => {
|
|
346
|
+
const richText = createRichText();
|
|
347
|
+
expect(enableTextFieldSignals(richText)).toBe(enableTextFieldSignals(richText));
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('enables onTextFieldChange emission from setters', () => {
|
|
351
|
+
const richText = createRichText({ data: { text: 'a' } });
|
|
352
|
+
const signals = enableTextFieldSignals(richText);
|
|
353
|
+
const changes: string[] = [];
|
|
354
|
+
connectSignal(signals.onTextFieldChange, (event) => {
|
|
355
|
+
changes.push(event.previousText);
|
|
356
|
+
});
|
|
357
|
+
setRichTextString(richText, 'b');
|
|
358
|
+
expect(changes).toEqual(['a']);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
describe('getRichTextBottomScrollV', () => {
|
|
363
|
+
afterEach(() => {
|
|
364
|
+
setTextLayoutMeasureProvider(null);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('returns 1 when no measure provider is registered', () => {
|
|
368
|
+
const richText = createRichText();
|
|
369
|
+
setRichTextString(richText, 'hello');
|
|
370
|
+
expect(getRichTextBottomScrollV(richText)).toBe(1);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('returns a value >= 1 when layout is available', () => {
|
|
374
|
+
setTextLayoutMeasureProvider((text) => text.length * 7);
|
|
375
|
+
const richText = createRichText({ data: { height: 20 } });
|
|
376
|
+
setRichTextString(richText, 'line1\nline2\nline3');
|
|
377
|
+
expect(getRichTextBottomScrollV(richText)).toBeGreaterThanOrEqual(1);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
describe('getRichTextCharIndexAtPoint', () => {
|
|
382
|
+
afterEach(() => {
|
|
383
|
+
setTextLayoutMeasureProvider(null);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('returns -1 when no measure provider is registered', () => {
|
|
387
|
+
const richText = createRichText();
|
|
388
|
+
setRichTextString(richText, 'hello');
|
|
389
|
+
expect(getRichTextCharIndexAtPoint(richText, 5, 5)).toBe(-1);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('returns a valid index when layout is available and point is past last line', () => {
|
|
393
|
+
setTextLayoutMeasureProvider((text) => text.length * 7);
|
|
394
|
+
const richText = createRichText({ data: { width: 200, height: 50 } });
|
|
395
|
+
setRichTextString(richText, 'hello');
|
|
396
|
+
// Use a y well beyond the text height to hit the end-of-field code path (avoids a known
|
|
397
|
+
// textlayout bug in the mid-line hit-test branch that references `text` instead of `_text`).
|
|
398
|
+
const index = getRichTextCharIndexAtPoint(richText, 0, 9999);
|
|
399
|
+
expect(index).toBeGreaterThanOrEqual(0);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe('getRichTextDefaultTextFormat', () => {
|
|
404
|
+
it('returns the default text format', () => {
|
|
405
|
+
const format = { size: 16, bold: true };
|
|
406
|
+
const richText = createRichText({ data: { defaultTextFormat: format } });
|
|
407
|
+
expect(getRichTextDefaultTextFormat(richText)).toBe(richText.data.defaultTextFormat);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
describe('getRichTextFormatRangeAt', () => {
|
|
412
|
+
it('returns empty object for an empty field with no ranges', () => {
|
|
413
|
+
const richText = createRichText();
|
|
414
|
+
const out: Record<string, unknown> = {};
|
|
415
|
+
getRichTextFormatRangeAt(out, richText, 0);
|
|
416
|
+
expect(out).toEqual({});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('merges defaultTextFormat with overlapping ranges', () => {
|
|
420
|
+
const richText = createRichText({
|
|
421
|
+
data: {
|
|
422
|
+
defaultTextFormat: { size: 12, color: 0xff0000 },
|
|
423
|
+
text: 'hello world',
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
setRichTextFormatRange(richText, { bold: true }, 0, 5);
|
|
427
|
+
const out: Record<string, unknown> = {};
|
|
428
|
+
getRichTextFormatRangeAt(out, richText, 2);
|
|
429
|
+
expect(out.size).toBe(12);
|
|
430
|
+
expect(out.color).toBe(0xff0000);
|
|
431
|
+
expect(out.bold).toBe(true);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('does not apply a range that does not cover the index', () => {
|
|
435
|
+
const richText = createRichText({ data: { text: 'hello world' } });
|
|
436
|
+
setRichTextFormatRange(richText, { bold: true }, 0, 3);
|
|
437
|
+
const out: Record<string, unknown> = {};
|
|
438
|
+
getRichTextFormatRangeAt(out, richText, 5);
|
|
439
|
+
expect(out.bold).toBeUndefined();
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe('getRichTextFormatRangeByIndex', () => {
|
|
444
|
+
it('returns false for an out-of-bounds index', () => {
|
|
445
|
+
const richText = createRichText();
|
|
446
|
+
const out = { start: 0, end: 0, format: {} };
|
|
447
|
+
expect(getRichTextFormatRangeByIndex(out, richText, 0)).toBe(false);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('returns true and fills out with range data for a valid index', () => {
|
|
451
|
+
const richText = createRichText({ data: { text: 'hello' } });
|
|
452
|
+
setRichTextFormatRange(richText, { italic: true }, 1, 4);
|
|
453
|
+
const out = { start: 0, end: 0, format: {} };
|
|
454
|
+
expect(getRichTextFormatRangeByIndex(out, richText, 0)).toBe(true);
|
|
455
|
+
expect(out.start).toBe(1);
|
|
456
|
+
expect(out.end).toBe(4);
|
|
457
|
+
expect(out.format).toEqual({ italic: true });
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
describe('getRichTextFormatRangeCount', () => {
|
|
462
|
+
it('returns 0 for a field with no ranges', () => {
|
|
463
|
+
const richText = createRichText();
|
|
464
|
+
expect(getRichTextFormatRangeCount(richText)).toBe(0);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('returns the number of pushed ranges', () => {
|
|
468
|
+
const richText = createRichText({ data: { text: 'hello' } });
|
|
469
|
+
setRichTextFormatRange(richText, { bold: true }, 0, 2);
|
|
470
|
+
setRichTextFormatRange(richText, { italic: true }, 2, 5);
|
|
471
|
+
expect(getRichTextFormatRangeCount(richText)).toBe(2);
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
describe('getRichTextFormatRangesIn', () => {
|
|
476
|
+
it('clears out and leaves it empty when no ranges overlap the span', () => {
|
|
477
|
+
const richText = createRichText({ data: { text: 'hello world' } });
|
|
478
|
+
setRichTextFormatRange(richText, { bold: true }, 6, 11);
|
|
479
|
+
const out: TextFormatRange[] = [{ start: 99, end: 99, format: {} }];
|
|
480
|
+
getRichTextFormatRangesIn(out, richText, 0, 5);
|
|
481
|
+
expect(out).toHaveLength(0);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('collects every range overlapping the half-open span in stored order', () => {
|
|
485
|
+
const richText = createRichText({ data: { text: 'hello world' } });
|
|
486
|
+
setRichTextFormatRange(richText, { bold: true }, 0, 5);
|
|
487
|
+
setRichTextFormatRange(richText, { italic: true }, 4, 8);
|
|
488
|
+
setRichTextFormatRange(richText, { underline: true }, 8, 11);
|
|
489
|
+
const out: TextFormatRange[] = [];
|
|
490
|
+
getRichTextFormatRangesIn(out, richText, 3, 6);
|
|
491
|
+
expect(out).toHaveLength(2);
|
|
492
|
+
expect(out[0].format).toEqual({ bold: true });
|
|
493
|
+
expect(out[1].format).toEqual({ italic: true });
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it('excludes a range that only touches the span boundaries (half-open)', () => {
|
|
497
|
+
const richText = createRichText({ data: { text: 'hello world' } });
|
|
498
|
+
setRichTextFormatRange(richText, { bold: true }, 5, 8);
|
|
499
|
+
const out: TextFormatRange[] = [];
|
|
500
|
+
// span [0, 5): range starting at 5 does not overlap.
|
|
501
|
+
getRichTextFormatRangesIn(out, richText, 0, 5);
|
|
502
|
+
expect(out).toHaveLength(0);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('pushes ranges by reference', () => {
|
|
506
|
+
const richText = createRichText({ data: { text: 'hello' } });
|
|
507
|
+
setRichTextFormatRange(richText, { bold: true }, 0, 5);
|
|
508
|
+
const out: TextFormatRange[] = [];
|
|
509
|
+
getRichTextFormatRangesIn(out, richText, 0, 5);
|
|
510
|
+
expect(out[0]).toBe(richText.data.textFormatRanges[0]);
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
describe('getRichTextHtml', () => {
|
|
515
|
+
it('returns the htmlText field', () => {
|
|
516
|
+
const richText = createRichText({ data: { htmlText: '<b>hi</b>' } });
|
|
517
|
+
expect(getRichTextHtml(richText)).toBe('<b>hi</b>');
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
describe('getRichTextLength', () => {
|
|
522
|
+
it('returns 0 for an empty field', () => {
|
|
523
|
+
expect(getRichTextLength(createRichText())).toBe(0);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it('returns the character count of the text', () => {
|
|
527
|
+
const richText = createRichText({ data: { text: 'hello' } });
|
|
528
|
+
expect(getRichTextLength(richText)).toBe(5);
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
describe('getRichTextLineCount', () => {
|
|
533
|
+
afterEach(() => {
|
|
534
|
+
setTextLayoutMeasureProvider(null);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('returns 0 when no measure provider is registered', () => {
|
|
538
|
+
const richText = createRichText();
|
|
539
|
+
setRichTextString(richText, 'hello');
|
|
540
|
+
expect(getRichTextLineCount(richText)).toBe(0);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('returns >= 1 when layout is available', () => {
|
|
544
|
+
setTextLayoutMeasureProvider((text) => text.length * 7);
|
|
545
|
+
const richText = createRichText({ data: { width: 200 } });
|
|
546
|
+
setRichTextString(richText, 'hello');
|
|
547
|
+
expect(getRichTextLineCount(richText)).toBeGreaterThanOrEqual(1);
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
describe('getRichTextLineMetrics', () => {
|
|
552
|
+
afterEach(() => {
|
|
553
|
+
setTextLayoutMeasureProvider(null);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it('returns null when no measure provider is registered', () => {
|
|
557
|
+
const richText = createRichText();
|
|
558
|
+
setRichTextString(richText, 'hello');
|
|
559
|
+
expect(getRichTextLineMetrics(richText, 0)).toBeNull();
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('returns metrics for line 0 when layout is available', () => {
|
|
563
|
+
setTextLayoutMeasureProvider((text) => text.length * 7);
|
|
564
|
+
const richText = createRichText({ data: { width: 200 } });
|
|
565
|
+
setRichTextString(richText, 'hello');
|
|
566
|
+
const metrics = getRichTextLineMetrics(richText, 0);
|
|
567
|
+
expect(metrics).not.toBeNull();
|
|
568
|
+
expect(metrics!.width).toBeGreaterThan(0);
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
describe('getRichTextMaxScrollH', () => {
|
|
573
|
+
afterEach(() => {
|
|
574
|
+
setTextLayoutMeasureProvider(null);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('returns 0 when no measure provider is registered', () => {
|
|
578
|
+
const richText = createRichText();
|
|
579
|
+
setRichTextString(richText, 'hello');
|
|
580
|
+
expect(getRichTextMaxScrollH(richText)).toBe(0);
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
describe('getRichTextMaxScrollV', () => {
|
|
585
|
+
afterEach(() => {
|
|
586
|
+
setTextLayoutMeasureProvider(null);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('returns 1 when no measure provider is registered', () => {
|
|
590
|
+
const richText = createRichText();
|
|
591
|
+
setRichTextString(richText, 'hello');
|
|
592
|
+
expect(getRichTextMaxScrollV(richText)).toBe(1);
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
describe('getRichTextPasswordCharacter', () => {
|
|
597
|
+
it('returns null for a static RichText with no input slot', () => {
|
|
598
|
+
expect(getRichTextPasswordCharacter(createRichText())).toBeNull();
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('returns the mask character when the input slot enables password display', () => {
|
|
602
|
+
const richText = createRichText();
|
|
603
|
+
(getRichTextRuntime(richText) as RichTextRuntime).input = {
|
|
604
|
+
alwaysShowSelection: false,
|
|
605
|
+
caretColor: 0x000000,
|
|
606
|
+
caretIndex: 0,
|
|
607
|
+
caretWidth: 1,
|
|
608
|
+
desiredCaretX: -1,
|
|
609
|
+
displayAsPassword: true,
|
|
610
|
+
focused: false,
|
|
611
|
+
history: [],
|
|
612
|
+
historyIndex: -1,
|
|
613
|
+
historyLimit: 100,
|
|
614
|
+
passwordCharacter: '*',
|
|
615
|
+
restrict: '',
|
|
616
|
+
selectionAlpha: 0.35,
|
|
617
|
+
selectionColor: 0,
|
|
618
|
+
selectionIndex: 0,
|
|
619
|
+
};
|
|
620
|
+
expect(getRichTextPasswordCharacter(richText)).toBe('*');
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('returns null when the input slot has password display off', () => {
|
|
624
|
+
const richText = createRichText();
|
|
625
|
+
(getRichTextRuntime(richText) as RichTextRuntime).input = {
|
|
626
|
+
alwaysShowSelection: false,
|
|
627
|
+
caretColor: 0x000000,
|
|
628
|
+
caretIndex: 0,
|
|
629
|
+
caretWidth: 1,
|
|
630
|
+
desiredCaretX: -1,
|
|
631
|
+
displayAsPassword: false,
|
|
632
|
+
focused: false,
|
|
633
|
+
history: [],
|
|
634
|
+
historyIndex: -1,
|
|
635
|
+
historyLimit: 100,
|
|
636
|
+
passwordCharacter: '*',
|
|
637
|
+
restrict: '',
|
|
638
|
+
selectionAlpha: 0.35,
|
|
639
|
+
selectionColor: 0,
|
|
640
|
+
selectionIndex: 0,
|
|
641
|
+
};
|
|
642
|
+
expect(getRichTextPasswordCharacter(richText)).toBeNull();
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
describe('getRichTextRuntime', () => {
|
|
647
|
+
it('returns the runtime for a RichText', () => {
|
|
648
|
+
const richText = createRichText();
|
|
649
|
+
const runtime = getRichTextRuntime(richText);
|
|
650
|
+
expect(runtime).not.toBeNull();
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
describe('getRichTextString', () => {
|
|
655
|
+
it('returns the text field', () => {
|
|
656
|
+
const richText = createRichText({ data: { text: 'hello' } });
|
|
657
|
+
expect(getRichTextString(richText)).toBe('hello');
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
describe('getRichTextTextHeight', () => {
|
|
662
|
+
afterEach(() => {
|
|
663
|
+
setTextLayoutMeasureProvider(null);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it('returns 0 when no measure provider is registered', () => {
|
|
667
|
+
const richText = createRichText();
|
|
668
|
+
setRichTextString(richText, 'hello');
|
|
669
|
+
expect(getRichTextTextHeight(richText)).toBe(0);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it('returns a positive value when layout is available', () => {
|
|
673
|
+
setTextLayoutMeasureProvider((text) => text.length * 7);
|
|
674
|
+
const richText = createRichText({ data: { width: 200 } });
|
|
675
|
+
setRichTextString(richText, 'hello');
|
|
676
|
+
expect(getRichTextTextHeight(richText)).toBeGreaterThan(0);
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
describe('getRichTextTextWidth', () => {
|
|
681
|
+
afterEach(() => {
|
|
682
|
+
setTextLayoutMeasureProvider(null);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it('returns 0 when no measure provider is registered', () => {
|
|
686
|
+
const richText = createRichText();
|
|
687
|
+
setRichTextString(richText, 'hello');
|
|
688
|
+
expect(getRichTextTextWidth(richText)).toBe(0);
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it('returns a positive value when layout is available', () => {
|
|
692
|
+
setTextLayoutMeasureProvider((text) => text.length * 7);
|
|
693
|
+
const richText = createRichText({ data: { width: 200 } });
|
|
694
|
+
setRichTextString(richText, 'hello');
|
|
695
|
+
expect(getRichTextTextWidth(richText)).toBeGreaterThan(0);
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
describe('getTextFieldSignals', () => {
|
|
700
|
+
it('returns null before signals are enabled', () => {
|
|
701
|
+
expect(getTextFieldSignals(createRichText())).toBeNull();
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it('returns the enabled signals group', () => {
|
|
705
|
+
const richText = createRichText();
|
|
706
|
+
const signals = enableTextFieldSignals(richText);
|
|
707
|
+
expect(getTextFieldSignals(richText)).toBe(signals);
|
|
708
|
+
});
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
describe('insertRichTextString', () => {
|
|
712
|
+
it('inserts the value at the clamped index', () => {
|
|
713
|
+
const richText = createRichText({ data: { text: 'held' } });
|
|
714
|
+
insertRichTextString(richText, 2, 'LLO wor');
|
|
715
|
+
expect(richText.data.text).toBe('heLLO world');
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it('clamps the index into [0, text.length]', () => {
|
|
719
|
+
const richText = createRichText({ data: { text: 'abc' } });
|
|
720
|
+
insertRichTextString(richText, 999, 'XYZ');
|
|
721
|
+
expect(richText.data.text).toBe('abcXYZ');
|
|
722
|
+
insertRichTextString(richText, -5, 'Q');
|
|
723
|
+
expect(richText.data.text).toBe('QabcXYZ');
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it('does nothing for an empty value', () => {
|
|
727
|
+
const richText = createRichText({ data: { text: 'abc' } });
|
|
728
|
+
const runtime = getEntityRuntime(richText) as RichTextRuntime;
|
|
729
|
+
const idBefore = runtime.localContentId;
|
|
730
|
+
insertRichTextString(richText, 1, '');
|
|
731
|
+
expect(richText.data.text).toBe('abc');
|
|
732
|
+
expect(runtime.localContentId).toBe(idBefore);
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
it('shifts ranges starting at or after the insertion point', () => {
|
|
736
|
+
const richText = createRichText({ data: { text: 'abcdef' } });
|
|
737
|
+
setRichTextFormatRange(richText, { bold: true }, 3, 5);
|
|
738
|
+
insertRichTextString(richText, 0, 'XX');
|
|
739
|
+
const range = richText.data.textFormatRanges[0];
|
|
740
|
+
expect(range.start).toBe(5);
|
|
741
|
+
expect(range.end).toBe(7);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it('extends a range that straddles the insertion point', () => {
|
|
745
|
+
const richText = createRichText({ data: { text: 'abcdef' } });
|
|
746
|
+
setRichTextFormatRange(richText, { bold: true }, 1, 5);
|
|
747
|
+
insertRichTextString(richText, 3, 'XX');
|
|
748
|
+
const range = richText.data.textFormatRanges[0];
|
|
749
|
+
expect(range.start).toBe(1);
|
|
750
|
+
expect(range.end).toBe(7);
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
it('emits onTextFieldChange with the previous text when signals are enabled', () => {
|
|
754
|
+
const richText = createRichText({ data: { text: 'abc' } });
|
|
755
|
+
const signals = enableTextFieldSignals(richText);
|
|
756
|
+
let previous = '';
|
|
757
|
+
connectSignal(signals.onTextFieldChange, (event) => {
|
|
758
|
+
previous = event.previousText;
|
|
759
|
+
});
|
|
760
|
+
insertRichTextString(richText, 1, 'Z');
|
|
761
|
+
expect(previous).toBe('abc');
|
|
762
|
+
expect(richText.data.text).toBe('aZbc');
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
describe('removeRichTextFormatRangesIn', () => {
|
|
767
|
+
it('removes ranges that overlap the given span', () => {
|
|
768
|
+
const richText = createRichText({ data: { text: 'hello world' } });
|
|
769
|
+
setRichTextFormatRange(richText, { bold: true }, 0, 5);
|
|
770
|
+
setRichTextFormatRange(richText, { italic: true }, 6, 11);
|
|
771
|
+
removeRichTextFormatRangesIn(richText, 0, 5);
|
|
772
|
+
expect(richText.data.textFormatRanges).toHaveLength(1);
|
|
773
|
+
expect(richText.data.textFormatRanges[0].format).toEqual({ italic: true });
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
it('does not invalidate when no ranges overlap', () => {
|
|
777
|
+
const richText = createRichText({ data: { text: 'hello world' } });
|
|
778
|
+
setRichTextFormatRange(richText, { bold: true }, 6, 11);
|
|
779
|
+
const runtime = getEntityRuntime(richText) as RichTextRuntime;
|
|
780
|
+
const idBefore = runtime.localContentId;
|
|
781
|
+
removeRichTextFormatRangesIn(richText, 0, 5);
|
|
782
|
+
expect(runtime.localContentId).toBe(idBefore);
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
describe('replaceRichTextString', () => {
|
|
787
|
+
it('replaces the substring in [beginIndex, endIndex)', () => {
|
|
788
|
+
const richText = createRichText({ data: { text: 'hello world' } });
|
|
789
|
+
replaceRichTextString(richText, 0, 5, 'goodbye');
|
|
790
|
+
expect(richText.data.text).toBe('goodbye world');
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it('clamps indices and degenerates to an insert when begin >= end', () => {
|
|
794
|
+
const richText = createRichText({ data: { text: 'abc' } });
|
|
795
|
+
replaceRichTextString(richText, 5, 1, 'XYZ');
|
|
796
|
+
// begin clamps to 3, end clamps to max(begin, 1) = 3 -> insert at 3.
|
|
797
|
+
expect(richText.data.text).toBe('abcXYZ');
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
it('shifts ranges entirely after the replaced span by the net delta', () => {
|
|
801
|
+
const richText = createRichText({ data: { text: 'abcdefgh' } });
|
|
802
|
+
setRichTextFormatRange(richText, { bold: true }, 6, 8);
|
|
803
|
+
replaceRichTextString(richText, 0, 2, 'X');
|
|
804
|
+
const range = richText.data.textFormatRanges[0];
|
|
805
|
+
expect(range.start).toBe(5);
|
|
806
|
+
expect(range.end).toBe(7);
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it('leaves ranges entirely before the span unchanged', () => {
|
|
810
|
+
const richText = createRichText({ data: { text: 'abcdefgh' } });
|
|
811
|
+
setRichTextFormatRange(richText, { bold: true }, 0, 2);
|
|
812
|
+
replaceRichTextString(richText, 4, 6, 'XXXX');
|
|
813
|
+
const range = richText.data.textFormatRanges[0];
|
|
814
|
+
expect(range.start).toBe(0);
|
|
815
|
+
expect(range.end).toBe(2);
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
it('removes ranges fully inside the replaced span', () => {
|
|
819
|
+
const richText = createRichText({ data: { text: 'abcdefgh' } });
|
|
820
|
+
setRichTextFormatRange(richText, { bold: true }, 2, 5);
|
|
821
|
+
replaceRichTextString(richText, 1, 6, 'X');
|
|
822
|
+
expect(richText.data.textFormatRanges).toHaveLength(0);
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
it('shrinks a range that spans both boundaries by the net delta', () => {
|
|
826
|
+
const richText = createRichText({ data: { text: 'abcdefgh' } });
|
|
827
|
+
setRichTextFormatRange(richText, { bold: true }, 0, 8);
|
|
828
|
+
replaceRichTextString(richText, 2, 4, 'X');
|
|
829
|
+
const range = richText.data.textFormatRanges[0];
|
|
830
|
+
expect(range.start).toBe(0);
|
|
831
|
+
expect(range.end).toBe(7);
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
it('trims a left-overlapping range to the end of the inserted text', () => {
|
|
835
|
+
const richText = createRichText({ data: { text: 'abcdefgh' } });
|
|
836
|
+
setRichTextFormatRange(richText, { bold: true }, 0, 4);
|
|
837
|
+
replaceRichTextString(richText, 2, 6, 'XYZ');
|
|
838
|
+
const range = richText.data.textFormatRanges[0];
|
|
839
|
+
expect(range.start).toBe(0);
|
|
840
|
+
expect(range.end).toBe(5);
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
it('trims a right-overlapping range to after the inserted text', () => {
|
|
844
|
+
const richText = createRichText({ data: { text: 'abcdefgh' } });
|
|
845
|
+
setRichTextFormatRange(richText, { bold: true }, 4, 8);
|
|
846
|
+
replaceRichTextString(richText, 2, 6, 'XYZ');
|
|
847
|
+
const range = richText.data.textFormatRanges[0];
|
|
848
|
+
expect(range.start).toBe(5);
|
|
849
|
+
expect(range.end).toBe(7);
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
it('emits onTextFieldChange only when the text actually changed', () => {
|
|
853
|
+
const richText = createRichText({ data: { text: 'abc' } });
|
|
854
|
+
const signals = enableTextFieldSignals(richText);
|
|
855
|
+
const changes: string[] = [];
|
|
856
|
+
connectSignal(signals.onTextFieldChange, (event) => {
|
|
857
|
+
changes.push(event.previousText);
|
|
858
|
+
});
|
|
859
|
+
replaceRichTextString(richText, 0, 3, 'abc');
|
|
860
|
+
expect(changes).toEqual([]);
|
|
861
|
+
replaceRichTextString(richText, 0, 3, 'xyz');
|
|
862
|
+
expect(changes).toEqual(['abc']);
|
|
863
|
+
});
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
describe('setRichTextBackground', () => {
|
|
867
|
+
it('sets background and bumps content', () => {
|
|
868
|
+
const richText = createRichText();
|
|
869
|
+
const content = getNodeLocalContentRevision(richText);
|
|
870
|
+
setRichTextBackground(richText, true);
|
|
871
|
+
expect(richText.data.background).toBe(true);
|
|
872
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content + 1);
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
it('does not bump content when the value is unchanged', () => {
|
|
876
|
+
const richText = createRichText();
|
|
877
|
+
const content = getNodeLocalContentRevision(richText);
|
|
878
|
+
setRichTextBackground(richText, false);
|
|
879
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content);
|
|
880
|
+
});
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
describe('setRichTextBackgroundColor', () => {
|
|
884
|
+
it('sets backgroundColor and bumps content', () => {
|
|
885
|
+
const richText = createRichText();
|
|
886
|
+
const content = getNodeLocalContentRevision(richText);
|
|
887
|
+
setRichTextBackgroundColor(richText, 0xff0000ff);
|
|
888
|
+
expect(richText.data.backgroundColor).toBe(0xff0000ff);
|
|
889
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content + 1);
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
it('does not bump content when the value is unchanged', () => {
|
|
893
|
+
const richText = createRichText();
|
|
894
|
+
const content = getNodeLocalContentRevision(richText);
|
|
895
|
+
setRichTextBackgroundColor(richText, richText.data.backgroundColor);
|
|
896
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content);
|
|
897
|
+
});
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
describe('setRichTextBorder', () => {
|
|
901
|
+
it('sets border and bumps content', () => {
|
|
902
|
+
const richText = createRichText();
|
|
903
|
+
const content = getNodeLocalContentRevision(richText);
|
|
904
|
+
setRichTextBorder(richText, true);
|
|
905
|
+
expect(richText.data.border).toBe(true);
|
|
906
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content + 1);
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
it('does not bump content when the value is unchanged', () => {
|
|
910
|
+
const richText = createRichText();
|
|
911
|
+
const content = getNodeLocalContentRevision(richText);
|
|
912
|
+
setRichTextBorder(richText, false);
|
|
913
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content);
|
|
914
|
+
});
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
describe('setRichTextBorderColor', () => {
|
|
918
|
+
it('sets borderColor and bumps content', () => {
|
|
919
|
+
const richText = createRichText();
|
|
920
|
+
const content = getNodeLocalContentRevision(richText);
|
|
921
|
+
setRichTextBorderColor(richText, 0x000000ff);
|
|
922
|
+
expect(richText.data.borderColor).toBe(0x000000ff);
|
|
923
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content + 1);
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
it('does not bump content when the value is unchanged', () => {
|
|
927
|
+
const richText = createRichText();
|
|
928
|
+
const content = getNodeLocalContentRevision(richText);
|
|
929
|
+
setRichTextBorderColor(richText, richText.data.borderColor);
|
|
930
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content);
|
|
931
|
+
});
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
describe('setRichTextCondenseWhite', () => {
|
|
935
|
+
it('sets condenseWhite and bumps content', () => {
|
|
936
|
+
const richText = createRichText();
|
|
937
|
+
const content = getNodeLocalContentRevision(richText);
|
|
938
|
+
setRichTextCondenseWhite(richText, true);
|
|
939
|
+
expect(richText.data.condenseWhite).toBe(true);
|
|
940
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content + 1);
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
it('does not bump content when the value is unchanged', () => {
|
|
944
|
+
const richText = createRichText();
|
|
945
|
+
const content = getNodeLocalContentRevision(richText);
|
|
946
|
+
setRichTextCondenseWhite(richText, false);
|
|
947
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content);
|
|
948
|
+
});
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
describe('setRichTextDefaultTextFormat', () => {
|
|
952
|
+
it('sets the defaultTextFormat and bumps content', () => {
|
|
953
|
+
const richText = createRichText();
|
|
954
|
+
const content = getNodeLocalContentRevision(richText);
|
|
955
|
+
const format = { size: 18, bold: true };
|
|
956
|
+
setRichTextDefaultTextFormat(richText, format);
|
|
957
|
+
expect(richText.data.defaultTextFormat).toBe(format);
|
|
958
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content + 1);
|
|
959
|
+
});
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
describe('setRichTextFormatRange', () => {
|
|
963
|
+
it('adds a serialized format range', () => {
|
|
964
|
+
const richText = createRichText({ data: { text: 'hello' } });
|
|
965
|
+
setRichTextFormatRange(richText, { italic: true }, 1, 4);
|
|
966
|
+
expect(richText.data.textFormatRanges).toEqual([{ start: 1, end: 4, format: { italic: true } }]);
|
|
967
|
+
});
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
describe('setRichTextHeight', () => {
|
|
971
|
+
it('sets height, bumps content, and invalidates local bounds', () => {
|
|
972
|
+
const richText = createRichText();
|
|
973
|
+
const content = getNodeLocalContentRevision(richText);
|
|
974
|
+
const bounds = getNodeLocalBoundsRevision(richText);
|
|
975
|
+
setRichTextHeight(richText, 250);
|
|
976
|
+
expect(richText.data.height).toBe(250);
|
|
977
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content + 1);
|
|
978
|
+
expect(getNodeLocalBoundsRevision(richText)).not.toBe(bounds);
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
it('does not bump content when the value is unchanged', () => {
|
|
982
|
+
const richText = createRichText();
|
|
983
|
+
const content = getNodeLocalContentRevision(richText);
|
|
984
|
+
setRichTextHeight(richText, richText.data.height);
|
|
985
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content);
|
|
986
|
+
});
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
describe('setRichTextHtml', () => {
|
|
990
|
+
it('sets htmlText and bumps content', () => {
|
|
991
|
+
const richText = createRichText();
|
|
992
|
+
const content = getNodeLocalContentRevision(richText);
|
|
993
|
+
setRichTextHtml(richText, '<b>bold</b>');
|
|
994
|
+
expect(richText.data.htmlText).toBe('<b>bold</b>');
|
|
995
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content + 1);
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
it('does not bump content when the value is unchanged', () => {
|
|
999
|
+
const richText = createRichText({ data: { htmlText: '<i>same</i>' } });
|
|
1000
|
+
const content = getNodeLocalContentRevision(richText);
|
|
1001
|
+
setRichTextHtml(richText, '<i>same</i>');
|
|
1002
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content);
|
|
1003
|
+
});
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
describe('setRichTextMaxChars', () => {
|
|
1007
|
+
it('sets maxChars and bumps content', () => {
|
|
1008
|
+
const richText = createRichText();
|
|
1009
|
+
const content = getNodeLocalContentRevision(richText);
|
|
1010
|
+
setRichTextMaxChars(richText, 50);
|
|
1011
|
+
expect(richText.data.maxChars).toBe(50);
|
|
1012
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content + 1);
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
it('does not bump content when the value is unchanged', () => {
|
|
1016
|
+
const richText = createRichText();
|
|
1017
|
+
const content = getNodeLocalContentRevision(richText);
|
|
1018
|
+
setRichTextMaxChars(richText, richText.data.maxChars);
|
|
1019
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content);
|
|
1020
|
+
});
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
describe('setRichTextMouseWheelEnabled', () => {
|
|
1024
|
+
it('sets mouseWheelEnabled and bumps content', () => {
|
|
1025
|
+
const richText = createRichText();
|
|
1026
|
+
const content = getNodeLocalContentRevision(richText);
|
|
1027
|
+
setRichTextMouseWheelEnabled(richText, false);
|
|
1028
|
+
expect(richText.data.mouseWheelEnabled).toBe(false);
|
|
1029
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content + 1);
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
it('does not bump content when the value is unchanged', () => {
|
|
1033
|
+
const richText = createRichText();
|
|
1034
|
+
const content = getNodeLocalContentRevision(richText);
|
|
1035
|
+
setRichTextMouseWheelEnabled(richText, true);
|
|
1036
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content);
|
|
1037
|
+
});
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
describe('setRichTextMultiline', () => {
|
|
1041
|
+
it('sets multiline, bumps content, and invalidates bounds', () => {
|
|
1042
|
+
const richText = createRichText();
|
|
1043
|
+
const content = getNodeLocalContentRevision(richText);
|
|
1044
|
+
const bounds = getNodeLocalBoundsRevision(richText);
|
|
1045
|
+
setRichTextMultiline(richText, false);
|
|
1046
|
+
expect(richText.data.multiline).toBe(false);
|
|
1047
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content + 1);
|
|
1048
|
+
expect(getNodeLocalBoundsRevision(richText)).not.toBe(bounds);
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
it('does not bump content when the value is unchanged', () => {
|
|
1052
|
+
const richText = createRichText();
|
|
1053
|
+
const content = getNodeLocalContentRevision(richText);
|
|
1054
|
+
setRichTextMultiline(richText, true);
|
|
1055
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content);
|
|
1056
|
+
});
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
describe('setRichTextScrollH', () => {
|
|
1060
|
+
it('sets scrollH to the given value', () => {
|
|
1061
|
+
const richText = createRichText();
|
|
1062
|
+
setRichTextScrollH(richText, 10);
|
|
1063
|
+
expect(richText.data.scrollH).toBe(10);
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
it('clamps scrollH to 0 minimum', () => {
|
|
1067
|
+
const richText = createRichText();
|
|
1068
|
+
setRichTextScrollH(richText, -5);
|
|
1069
|
+
expect(richText.data.scrollH).toBe(0);
|
|
1070
|
+
});
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
describe('setRichTextScrollV', () => {
|
|
1074
|
+
it('sets scrollV to the given value', () => {
|
|
1075
|
+
const richText = createRichText();
|
|
1076
|
+
setRichTextScrollV(richText, 4);
|
|
1077
|
+
expect(richText.data.scrollV).toBe(4);
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
it('clamps scrollV to 1 minimum', () => {
|
|
1081
|
+
const richText = createRichText();
|
|
1082
|
+
setRichTextScrollV(richText, 0);
|
|
1083
|
+
expect(richText.data.scrollV).toBe(1);
|
|
1084
|
+
});
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
describe('setRichTextSelectable', () => {
|
|
1088
|
+
it('sets selectable and bumps content', () => {
|
|
1089
|
+
const richText = createRichText();
|
|
1090
|
+
const content = getNodeLocalContentRevision(richText);
|
|
1091
|
+
setRichTextSelectable(richText, false);
|
|
1092
|
+
expect(richText.data.selectable).toBe(false);
|
|
1093
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content + 1);
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
it('does not bump content when the value is unchanged', () => {
|
|
1097
|
+
const richText = createRichText();
|
|
1098
|
+
const content = getNodeLocalContentRevision(richText);
|
|
1099
|
+
setRichTextSelectable(richText, true);
|
|
1100
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content);
|
|
1101
|
+
});
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
describe('setRichTextString', () => {
|
|
1105
|
+
it('sets the text', () => {
|
|
1106
|
+
const richText = createRichText();
|
|
1107
|
+
setRichTextString(richText, 'hello');
|
|
1108
|
+
expect(richText.data.text).toBe('hello');
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
it('invalidates local content', () => {
|
|
1112
|
+
const richText = createRichText();
|
|
1113
|
+
const runtime = getEntityRuntime(richText) as RichTextRuntime;
|
|
1114
|
+
const idBefore = runtime.localContentId;
|
|
1115
|
+
setRichTextString(richText, 'hello');
|
|
1116
|
+
expect(runtime.localContentId).not.toBe(idBefore);
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
it('does not invalidate bounds when the field is fixed (autoSize none)', () => {
|
|
1120
|
+
const richText = createRichText({ data: { autoSize: 'none' } });
|
|
1121
|
+
const runtime = getEntityRuntime(richText) as RichTextRuntime;
|
|
1122
|
+
const boundsBefore = runtime.localBoundsId;
|
|
1123
|
+
setRichTextString(richText, 'hello');
|
|
1124
|
+
expect(runtime.localBoundsId).toBe(boundsBefore);
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
it('invalidates bounds when autoSize resizes the field', () => {
|
|
1128
|
+
const richText = createRichText({ data: { autoSize: 'left' } });
|
|
1129
|
+
const runtime = getEntityRuntime(richText) as RichTextRuntime;
|
|
1130
|
+
const boundsBefore = runtime.localBoundsId;
|
|
1131
|
+
setRichTextString(richText, 'hello');
|
|
1132
|
+
expect(runtime.localBoundsId).not.toBe(boundsBefore);
|
|
1133
|
+
});
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
describe('setRichTextStyleSheet', () => {
|
|
1137
|
+
it('sets styleSheet and bumps content', () => {
|
|
1138
|
+
const richText = createRichText();
|
|
1139
|
+
const content = getNodeLocalContentRevision(richText);
|
|
1140
|
+
const sheet = { p: { color: 0xff0000 } };
|
|
1141
|
+
setRichTextStyleSheet(richText, sheet);
|
|
1142
|
+
expect(richText.data.styleSheet).toBe(sheet);
|
|
1143
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content + 1);
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
it('sets styleSheet to null and bumps content', () => {
|
|
1147
|
+
const richText = createRichText({ data: { styleSheet: { p: {} } } });
|
|
1148
|
+
const content = getNodeLocalContentRevision(richText);
|
|
1149
|
+
setRichTextStyleSheet(richText, null);
|
|
1150
|
+
expect(richText.data.styleSheet).toBeNull();
|
|
1151
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content + 1);
|
|
1152
|
+
});
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
describe('setRichTextTextColor', () => {
|
|
1156
|
+
it('sets textColor and bumps content', () => {
|
|
1157
|
+
const richText = createRichText();
|
|
1158
|
+
const content = getNodeLocalContentRevision(richText);
|
|
1159
|
+
setRichTextTextColor(richText, 0xff0000ff);
|
|
1160
|
+
expect(richText.data.textColor).toBe(0xff0000ff);
|
|
1161
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content + 1);
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
it('does not bump content when the value is unchanged', () => {
|
|
1165
|
+
const richText = createRichText();
|
|
1166
|
+
const content = getNodeLocalContentRevision(richText);
|
|
1167
|
+
setRichTextTextColor(richText, richText.data.textColor);
|
|
1168
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content);
|
|
1169
|
+
});
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
describe('setRichTextWidth', () => {
|
|
1173
|
+
it('sets width, bumps content, and invalidates local bounds', () => {
|
|
1174
|
+
const richText = createRichText();
|
|
1175
|
+
const content = getNodeLocalContentRevision(richText);
|
|
1176
|
+
const bounds = getNodeLocalBoundsRevision(richText);
|
|
1177
|
+
setRichTextWidth(richText, 300);
|
|
1178
|
+
expect(richText.data.width).toBe(300);
|
|
1179
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content + 1);
|
|
1180
|
+
expect(getNodeLocalBoundsRevision(richText)).not.toBe(bounds);
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
it('does not bump content when the value is unchanged', () => {
|
|
1184
|
+
const richText = createRichText();
|
|
1185
|
+
const content = getNodeLocalContentRevision(richText);
|
|
1186
|
+
setRichTextWidth(richText, richText.data.width);
|
|
1187
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content);
|
|
1188
|
+
});
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
describe('setRichTextWordWrap', () => {
|
|
1192
|
+
it('sets wordWrap, bumps content, and invalidates bounds', () => {
|
|
1193
|
+
const richText = createRichText();
|
|
1194
|
+
const content = getNodeLocalContentRevision(richText);
|
|
1195
|
+
const bounds = getNodeLocalBoundsRevision(richText);
|
|
1196
|
+
setRichTextWordWrap(richText, true);
|
|
1197
|
+
expect(richText.data.wordWrap).toBe(true);
|
|
1198
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content + 1);
|
|
1199
|
+
expect(getNodeLocalBoundsRevision(richText)).not.toBe(bounds);
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
it('does not bump content when the value is unchanged', () => {
|
|
1203
|
+
const richText = createRichText();
|
|
1204
|
+
const content = getNodeLocalContentRevision(richText);
|
|
1205
|
+
setRichTextWordWrap(richText, false);
|
|
1206
|
+
expect(getNodeLocalContentRevision(richText)).toBe(content);
|
|
1207
|
+
});
|
|
1208
|
+
});
|