@flighthq/scene2d-dom 0.2.1-next.840.857425c
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/domBackground.d.ts +3 -0
- package/dist/domBackground.d.ts.map +1 -0
- package/dist/domBackground.js +9 -0
- package/dist/domBackground.js.map +1 -0
- package/dist/domBitmap.d.ts +4 -0
- package/dist/domBitmap.d.ts.map +1 -0
- package/dist/domBitmap.js +81 -0
- package/dist/domBitmap.js.map +1 -0
- package/dist/domCSSFilterBinding.d.ts +19 -0
- package/dist/domCSSFilterBinding.d.ts.map +1 -0
- package/dist/domCSSFilterBinding.js +36 -0
- package/dist/domCSSFilterBinding.js.map +1 -0
- package/dist/domCache.d.ts +14 -0
- package/dist/domCache.d.ts.map +1 -0
- package/dist/domCache.js +64 -0
- package/dist/domCache.js.map +1 -0
- package/dist/domClip.d.ts +3 -0
- package/dist/domClip.d.ts.map +1 -0
- package/dist/domClip.js +38 -0
- package/dist/domClip.js.map +1 -0
- package/dist/domClipContours.d.ts +6 -0
- package/dist/domClipContours.d.ts.map +1 -0
- package/dist/domClipContours.js +48 -0
- package/dist/domClipContours.js.map +1 -0
- package/dist/domClipRectangle.d.ts +6 -0
- package/dist/domClipRectangle.d.ts.map +1 -0
- package/dist/domClipRectangle.js +148 -0
- package/dist/domClipRectangle.js.map +1 -0
- package/dist/domFontSource.d.ts +5 -0
- package/dist/domFontSource.d.ts.map +1 -0
- package/dist/domFontSource.js +18 -0
- package/dist/domFontSource.js.map +1 -0
- package/dist/domHtmlView.d.ts +4 -0
- package/dist/domHtmlView.d.ts.map +1 -0
- package/dist/domHtmlView.js +28 -0
- package/dist/domHtmlView.js.map +1 -0
- package/dist/domImageSource.d.ts +4 -0
- package/dist/domImageSource.d.ts.map +1 -0
- package/dist/domImageSource.js +36 -0
- package/dist/domImageSource.js.map +1 -0
- package/dist/domMaterials.d.ts +15 -0
- package/dist/domMaterials.d.ts.map +1 -0
- package/dist/domMaterials.js +68 -0
- package/dist/domMaterials.js.map +1 -0
- package/dist/domNativeText.d.ts +5 -0
- package/dist/domNativeText.d.ts.map +1 -0
- package/dist/domNativeText.js +88 -0
- package/dist/domNativeText.js.map +1 -0
- package/dist/domNode2D.d.ts +5 -0
- package/dist/domNode2D.d.ts.map +1 -0
- package/dist/domNode2D.js +60 -0
- package/dist/domNode2D.js.map +1 -0
- package/dist/domReconcile.d.ts +9 -0
- package/dist/domReconcile.d.ts.map +1 -0
- package/dist/domReconcile.js +69 -0
- package/dist/domReconcile.js.map +1 -0
- package/dist/domRenderState.d.ts +5 -0
- package/dist/domRenderState.d.ts.map +1 -0
- package/dist/domRenderState.js +43 -0
- package/dist/domRenderState.js.map +1 -0
- package/dist/domRenderView.d.ts +4 -0
- package/dist/domRenderView.d.ts.map +1 -0
- package/dist/domRenderView.js +24 -0
- package/dist/domRenderView.js.map +1 -0
- package/dist/domRichText.d.ts +6 -0
- package/dist/domRichText.d.ts.map +1 -0
- package/dist/domRichText.js +184 -0
- package/dist/domRichText.js.map +1 -0
- package/dist/domScale9Mapper.d.ts +3 -0
- package/dist/domScale9Mapper.d.ts.map +1 -0
- package/dist/domScale9Mapper.js +37 -0
- package/dist/domScale9Mapper.js.map +1 -0
- package/dist/domScale9Shape.d.ts +10 -0
- package/dist/domScale9Shape.d.ts.map +1 -0
- package/dist/domScale9Shape.js +59 -0
- package/dist/domScale9Shape.js.map +1 -0
- package/dist/domShape.d.ts +4 -0
- package/dist/domShape.d.ts.map +1 -0
- package/dist/domShape.js +45 -0
- package/dist/domShape.js.map +1 -0
- package/dist/domStyle.d.ts +9 -0
- package/dist/domStyle.d.ts.map +1 -0
- package/dist/domStyle.js +25 -0
- package/dist/domStyle.js.map +1 -0
- package/dist/domTextHelpers.d.ts +6 -0
- package/dist/domTextHelpers.d.ts.map +1 -0
- package/dist/domTextHelpers.js +8 -0
- package/dist/domTextHelpers.js.map +1 -0
- package/dist/domTextInput.d.ts +4 -0
- package/dist/domTextInput.d.ts.map +1 -0
- package/dist/domTextInput.js +60 -0
- package/dist/domTextInput.js.map +1 -0
- package/dist/domTextLabel.d.ts +4 -0
- package/dist/domTextLabel.d.ts.map +1 -0
- package/dist/domTextLabel.js +75 -0
- package/dist/domTextLabel.js.map +1 -0
- package/dist/domTransform.d.ts +4 -0
- package/dist/domTransform.d.ts.map +1 -0
- package/dist/domTransform.js +16 -0
- package/dist/domTransform.js.map +1 -0
- package/dist/domVideo.d.ts +4 -0
- package/dist/domVideo.d.ts.map +1 -0
- package/dist/domVideo.js +27 -0
- package/dist/domVideo.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
- package/src/domBackground.test.ts +35 -0
- package/src/domBitmap.test.ts +117 -0
- package/src/domCSSFilterBinding.test.ts +62 -0
- package/src/domCache.test.ts +93 -0
- package/src/domClip.test.ts +12 -0
- package/src/domClipContours.test.ts +62 -0
- package/src/domClipRectangle.test.ts +81 -0
- package/src/domFontSource.test.ts +30 -0
- package/src/domHtmlView.test.ts +117 -0
- package/src/domImageSource.test.ts +47 -0
- package/src/domMaterials.test.ts +100 -0
- package/src/domNativeText.test.ts +102 -0
- package/src/domNode2D.test.ts +236 -0
- package/src/domReconcile.test.ts +158 -0
- package/src/domRenderState.test.ts +87 -0
- package/src/domRenderView.test.ts +69 -0
- package/src/domRichText.test.ts +169 -0
- package/src/domScale9Mapper.test.ts +20 -0
- package/src/domScale9Shape.test.ts +76 -0
- package/src/domShape.test.ts +85 -0
- package/src/domStyle.test.ts +130 -0
- package/src/domTextHelpers.test.ts +27 -0
- package/src/domTextInput.test.ts +61 -0
- package/src/domTextLabel.test.ts +92 -0
- package/src/domTransform.test.ts +68 -0
- package/src/domVideo.test.ts +30 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { registerRenderer } from '@flighthq/render';
|
|
2
|
+
import { getOrCreateRenderProxy2D } from '@flighthq/render';
|
|
3
|
+
import { createRichText } from '@flighthq/text';
|
|
4
|
+
import { enableTextInput } from '@flighthq/textinput';
|
|
5
|
+
import { RichTextKind } from '@flighthq/types';
|
|
6
|
+
|
|
7
|
+
import { createDomRenderState, getDomRenderStateRuntime } from './domRenderState';
|
|
8
|
+
import {
|
|
9
|
+
defaultDomRichTextRenderer,
|
|
10
|
+
drawDomRichText,
|
|
11
|
+
drawDomRichTextMask,
|
|
12
|
+
registerDomTextInputOverlay,
|
|
13
|
+
} from './domRichText';
|
|
14
|
+
|
|
15
|
+
function makeState() {
|
|
16
|
+
const container = document.createElement('div');
|
|
17
|
+
const state = createDomRenderState(container);
|
|
18
|
+
registerRenderer(state, RichTextKind, defaultDomRichTextRenderer);
|
|
19
|
+
return state;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function drawGetEl(state: ReturnType<typeof makeState>, drawFn: () => void): HTMLElement | null {
|
|
23
|
+
getDomRenderStateRuntime(state).domCurrentElement = null;
|
|
24
|
+
drawFn();
|
|
25
|
+
return getDomRenderStateRuntime(state).domCurrentElement;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('defaultDomRichTextRenderer', () => {
|
|
29
|
+
it('has submit, and createData', () => {
|
|
30
|
+
expect(typeof defaultDomRichTextRenderer.submit).toBe('function');
|
|
31
|
+
expect(typeof defaultDomRichTextRenderer.createData).toBe('function');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('drawDomRichText', () => {
|
|
36
|
+
it('does not throw when text is empty', () => {
|
|
37
|
+
const state = makeState();
|
|
38
|
+
const node = createRichText();
|
|
39
|
+
node.data.text = '';
|
|
40
|
+
const renderProxy = getOrCreateRenderProxy2D(state, node);
|
|
41
|
+
expect(() => drawDomRichText(state, renderProxy)).not.toThrow();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('produces a div even when text is empty', () => {
|
|
45
|
+
const state = makeState();
|
|
46
|
+
const node = createRichText();
|
|
47
|
+
node.data.text = '';
|
|
48
|
+
const renderProxy = getOrCreateRenderProxy2D(state, node);
|
|
49
|
+
|
|
50
|
+
const el = drawGetEl(state, () => drawDomRichText(state, renderProxy));
|
|
51
|
+
|
|
52
|
+
expect(el).not.toBeNull();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('clears innerHtml when text is empty', () => {
|
|
56
|
+
const state = makeState();
|
|
57
|
+
const node = createRichText();
|
|
58
|
+
node.data.text = 'hello';
|
|
59
|
+
const renderProxy = getOrCreateRenderProxy2D(state, node);
|
|
60
|
+
drawDomRichText(state, renderProxy);
|
|
61
|
+
|
|
62
|
+
node.data.text = '';
|
|
63
|
+
const div = drawGetEl(state, () => drawDomRichText(state, renderProxy));
|
|
64
|
+
|
|
65
|
+
expect(div!.innerHTML).toBe('');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('produces a div when text is non-empty', () => {
|
|
69
|
+
const state = makeState();
|
|
70
|
+
const node = createRichText();
|
|
71
|
+
node.data.text = 'hello';
|
|
72
|
+
const renderProxy = getOrCreateRenderProxy2D(state, node);
|
|
73
|
+
|
|
74
|
+
const el = drawGetEl(state, () => drawDomRichText(state, renderProxy));
|
|
75
|
+
|
|
76
|
+
expect(el).not.toBeNull();
|
|
77
|
+
expect(el!.tagName).toBe('DIV');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('sets div width and height from source data', () => {
|
|
81
|
+
const state = makeState();
|
|
82
|
+
const node = createRichText();
|
|
83
|
+
node.data.text = 'hello';
|
|
84
|
+
node.data.width = 200;
|
|
85
|
+
node.data.height = 100;
|
|
86
|
+
const renderProxy = getOrCreateRenderProxy2D(state, node);
|
|
87
|
+
|
|
88
|
+
const div = drawGetEl(state, () => drawDomRichText(state, renderProxy))!;
|
|
89
|
+
expect(div.style.width).toBe('200px');
|
|
90
|
+
expect(div.style.height).toBe('100px');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('includes the text content in innerHtml', () => {
|
|
94
|
+
const state = makeState();
|
|
95
|
+
const node = createRichText();
|
|
96
|
+
node.data.text = 'world';
|
|
97
|
+
const renderProxy = getOrCreateRenderProxy2D(state, node);
|
|
98
|
+
|
|
99
|
+
const div = drawGetEl(state, () => drawDomRichText(state, renderProxy))!;
|
|
100
|
+
expect(div.innerHTML).toContain('world');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('renders resolved multi-format spans', () => {
|
|
104
|
+
const state = makeState();
|
|
105
|
+
const node = createRichText();
|
|
106
|
+
node.data.text = 'BoldGreen';
|
|
107
|
+
node.data.textFormatRanges = [
|
|
108
|
+
{ start: 0, end: 4, format: { bold: true } },
|
|
109
|
+
{ start: 4, end: 9, format: { color: 0x00ff00 } },
|
|
110
|
+
];
|
|
111
|
+
const renderProxy = getOrCreateRenderProxy2D(state, node);
|
|
112
|
+
|
|
113
|
+
const div = drawGetEl(state, () => drawDomRichText(state, renderProxy))!;
|
|
114
|
+
expect(div.innerHTML).toContain('Bold');
|
|
115
|
+
expect(div.innerHTML).toContain('Green');
|
|
116
|
+
expect(div.innerHTML).toContain('bold');
|
|
117
|
+
expect(div.innerHTML).toContain('#00ff00');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('sets backgroundColor when background is enabled', () => {
|
|
121
|
+
const state = makeState();
|
|
122
|
+
const node = createRichText();
|
|
123
|
+
node.data.text = 'hi';
|
|
124
|
+
node.data.background = true;
|
|
125
|
+
node.data.backgroundColor = 0xff0000;
|
|
126
|
+
const renderProxy = getOrCreateRenderProxy2D(state, node);
|
|
127
|
+
|
|
128
|
+
const div = drawGetEl(state, () => drawDomRichText(state, renderProxy))!;
|
|
129
|
+
expect(div.style.backgroundColor).not.toBe('');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('clears backgroundColor when background is disabled', () => {
|
|
133
|
+
const state = makeState();
|
|
134
|
+
const node = createRichText();
|
|
135
|
+
node.data.text = 'hi';
|
|
136
|
+
node.data.background = false;
|
|
137
|
+
const renderProxy = getOrCreateRenderProxy2D(state, node);
|
|
138
|
+
|
|
139
|
+
const div = drawGetEl(state, () => drawDomRichText(state, renderProxy))!;
|
|
140
|
+
expect(div.style.backgroundColor).toBe('');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('drawDomRichTextMask', () => {
|
|
145
|
+
it('does not throw and produces a DOM element', () => {
|
|
146
|
+
const state = makeState();
|
|
147
|
+
const node = createRichText({ data: { text: 'mask text' } });
|
|
148
|
+
const renderProxy = getOrCreateRenderProxy2D(state, node);
|
|
149
|
+
const el = drawGetEl(state, () => drawDomRichTextMask(state, renderProxy));
|
|
150
|
+
expect(el).not.toBeNull();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('registerDomTextInputOverlay', () => {
|
|
155
|
+
it('invokes the registered overlay only for a RichText with an input slot', () => {
|
|
156
|
+
const overlay = vi.fn();
|
|
157
|
+
registerDomTextInputOverlay(overlay);
|
|
158
|
+
const state = makeState();
|
|
159
|
+
|
|
160
|
+
const plain = createRichText({ data: { text: 'x' } });
|
|
161
|
+
drawDomRichText(state, getOrCreateRenderProxy2D(state, plain));
|
|
162
|
+
expect(overlay).not.toHaveBeenCalled();
|
|
163
|
+
|
|
164
|
+
const editable = createRichText({ data: { text: 'x' } });
|
|
165
|
+
enableTextInput(editable);
|
|
166
|
+
drawDomRichText(state, getOrCreateRenderProxy2D(state, editable));
|
|
167
|
+
expect(overlay).toHaveBeenCalled();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createRectangle } from '@flighthq/geometry';
|
|
2
|
+
|
|
3
|
+
import { buildDomScale9Mapper } from './domScale9Mapper';
|
|
4
|
+
|
|
5
|
+
describe('buildDomScale9Mapper', () => {
|
|
6
|
+
it('returns null for invalid scale or bounds', () => {
|
|
7
|
+
expect(buildDomScale9Mapper(createRectangle(0, 0, 0, 100), createRectangle(10, 10, 20, 20), 1, 1)).toBeNull();
|
|
8
|
+
expect(buildDomScale9Mapper(createRectangle(0, 0, 100, 100), createRectangle(10, 10, 20, 20), 0, 1)).toBeNull();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('maps edge and center coordinates using 9-slice scaling', () => {
|
|
12
|
+
const mapper = buildDomScale9Mapper(createRectangle(0, 0, 100, 100), createRectangle(10, 10, 20, 20), 2, 3)!;
|
|
13
|
+
|
|
14
|
+
expect(mapper.mapX(5)).toBe(5);
|
|
15
|
+
expect(mapper.mapX(20)).toBe(70);
|
|
16
|
+
expect(mapper.mapX(40)).toBe(140);
|
|
17
|
+
expect(mapper.mapY(20)).toBe(120);
|
|
18
|
+
expect(mapper.mapY(40)).toBe(240);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { getOrCreateRenderProxy2D } from '@flighthq/render';
|
|
2
|
+
import { appendShapeBeginFill, appendShapeRectangle, createScale9Shape } from '@flighthq/shape';
|
|
3
|
+
|
|
4
|
+
import { createDomRenderState, getDomRenderStateRuntime } from './domRenderState';
|
|
5
|
+
import { createDomScale9ShapeData, defaultDomScale9ShapeRenderer, drawDomScale9Shape } from './domScale9Shape';
|
|
6
|
+
|
|
7
|
+
const grid = { height: 80, width: 80, x: 10, y: 10 };
|
|
8
|
+
|
|
9
|
+
function getCurrentElement(state: ReturnType<typeof createDomRenderState>): HTMLElement | null {
|
|
10
|
+
return getDomRenderStateRuntime(state).domCurrentElement;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('createDomScale9ShapeData', () => {
|
|
14
|
+
it('creates empty renderer data', () => {
|
|
15
|
+
const state = createDomRenderState(document.createElement('div'));
|
|
16
|
+
const shape = createScale9Shape(grid);
|
|
17
|
+
|
|
18
|
+
const data = createDomScale9ShapeData(state, shape);
|
|
19
|
+
|
|
20
|
+
expect(data.canvas).toBeNull();
|
|
21
|
+
expect(data.context).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('defaultDomScale9ShapeRenderer', () => {
|
|
26
|
+
it('has submit, and createData functions', () => {
|
|
27
|
+
expect(defaultDomScale9ShapeRenderer.createData).toBe(createDomScale9ShapeData);
|
|
28
|
+
expect(defaultDomScale9ShapeRenderer.submit).toBe(drawDomScale9Shape);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('drawDomScale9Shape', () => {
|
|
33
|
+
it('returns early when commands are empty', () => {
|
|
34
|
+
const state = createDomRenderState(document.createElement('div'));
|
|
35
|
+
const shape = createScale9Shape(grid);
|
|
36
|
+
const data = getOrCreateRenderProxy2D(state, shape);
|
|
37
|
+
data.rendererData = createDomScale9ShapeData(state, shape);
|
|
38
|
+
|
|
39
|
+
drawDomScale9Shape(state, data);
|
|
40
|
+
|
|
41
|
+
expect(getCurrentElement(state)).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns early when rendererData is null', () => {
|
|
45
|
+
const state = createDomRenderState(document.createElement('div'));
|
|
46
|
+
const shape = createScale9Shape(grid);
|
|
47
|
+
appendShapeBeginFill(shape, 0xff0000);
|
|
48
|
+
appendShapeRectangle(shape, 0, 0, 100, 100);
|
|
49
|
+
const data = getOrCreateRenderProxy2D(state, shape);
|
|
50
|
+
|
|
51
|
+
drawDomScale9Shape(state, data);
|
|
52
|
+
|
|
53
|
+
expect(getCurrentElement(state)).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('renders remapped commands into a canvas and strips object scale from the transform', () => {
|
|
57
|
+
const state = createDomRenderState(document.createElement('div'));
|
|
58
|
+
const shape = createScale9Shape(grid);
|
|
59
|
+
shape.scaleX = 2;
|
|
60
|
+
shape.scaleY = 3;
|
|
61
|
+
appendShapeBeginFill(shape, 0xff0000);
|
|
62
|
+
appendShapeRectangle(shape, 0, 0, 100, 100);
|
|
63
|
+
const data = getOrCreateRenderProxy2D(state, shape);
|
|
64
|
+
data.rendererData = createDomScale9ShapeData(state, shape);
|
|
65
|
+
data.transform2D.a = 2;
|
|
66
|
+
data.transform2D.d = 3;
|
|
67
|
+
|
|
68
|
+
drawDomScale9Shape(state, data);
|
|
69
|
+
|
|
70
|
+
const element = getCurrentElement(state) as HTMLCanvasElement;
|
|
71
|
+
expect(element.tagName).toBe('CANVAS');
|
|
72
|
+
expect(element.width).toBe(200);
|
|
73
|
+
expect(element.height).toBe(300);
|
|
74
|
+
expect(element.style.transform).toBe('matrix(1,0,0,1,0,0)');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { registerRenderer } from '@flighthq/render';
|
|
2
|
+
import { getOrCreateRenderProxy2D } from '@flighthq/render';
|
|
3
|
+
import { defaultCanvasShapeCommands, registerCanvasShapeCommands } from '@flighthq/scene2d-canvas';
|
|
4
|
+
import { appendShapeBeginFill, appendShapeEndFill, appendShapeRectangle, createShape } from '@flighthq/shape';
|
|
5
|
+
import { ShapeKind } from '@flighthq/types';
|
|
6
|
+
|
|
7
|
+
import { createDomRenderState, getDomRenderStateRuntime } from './domRenderState';
|
|
8
|
+
import { defaultDomShapeRenderer, drawDomShape } from './domShape';
|
|
9
|
+
|
|
10
|
+
beforeAll(() => {
|
|
11
|
+
registerCanvasShapeCommands(defaultCanvasShapeCommands);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
function makeState() {
|
|
15
|
+
const container = document.createElement('div');
|
|
16
|
+
const state = createDomRenderState(container);
|
|
17
|
+
registerRenderer(state, ShapeKind, defaultDomShapeRenderer);
|
|
18
|
+
return state;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function drawGetEl(state: ReturnType<typeof makeState>, drawFn: () => void): HTMLElement | null {
|
|
22
|
+
getDomRenderStateRuntime(state).domCurrentElement = null;
|
|
23
|
+
drawFn();
|
|
24
|
+
return getDomRenderStateRuntime(state).domCurrentElement;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('defaultDomShapeRenderer', () => {
|
|
28
|
+
it('has submit, and createData', () => {
|
|
29
|
+
expect(typeof defaultDomShapeRenderer.submit).toBe('function');
|
|
30
|
+
expect(typeof defaultDomShapeRenderer.createData).toBe('function');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('drawDomShape', () => {
|
|
35
|
+
it('does not produce an element when commands array is empty', () => {
|
|
36
|
+
const state = makeState();
|
|
37
|
+
const shape = createShape();
|
|
38
|
+
const renderProxy = getOrCreateRenderProxy2D(state, shape);
|
|
39
|
+
|
|
40
|
+
const el = drawGetEl(state, () => drawDomShape(state, renderProxy));
|
|
41
|
+
|
|
42
|
+
expect(el).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('produces a canvas element when the shape has draw commands', () => {
|
|
46
|
+
const state = makeState();
|
|
47
|
+
const shape = createShape();
|
|
48
|
+
appendShapeBeginFill(shape, 0xff0000);
|
|
49
|
+
appendShapeRectangle(shape, 0, 0, 50, 50);
|
|
50
|
+
appendShapeEndFill(shape);
|
|
51
|
+
const renderProxy = getOrCreateRenderProxy2D(state, shape);
|
|
52
|
+
|
|
53
|
+
const el = drawGetEl(state, () => drawDomShape(state, renderProxy));
|
|
54
|
+
|
|
55
|
+
expect(el).not.toBeNull();
|
|
56
|
+
expect(el!.tagName).toBe('CANVAS');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('sets canvas size to at least 1x1 for zero-size shapes', () => {
|
|
60
|
+
const state = makeState();
|
|
61
|
+
const shape = createShape();
|
|
62
|
+
appendShapeBeginFill(shape, 0xff0000);
|
|
63
|
+
appendShapeRectangle(shape, 0, 0, 0, 0);
|
|
64
|
+
appendShapeEndFill(shape);
|
|
65
|
+
const renderProxy = getOrCreateRenderProxy2D(state, shape);
|
|
66
|
+
|
|
67
|
+
const canvas = drawGetEl(state, () => drawDomShape(state, renderProxy)) as HTMLCanvasElement;
|
|
68
|
+
expect(canvas.width).toBeGreaterThanOrEqual(1);
|
|
69
|
+
expect(canvas.height).toBeGreaterThanOrEqual(1);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('reuses the same canvas element across multiple draws', () => {
|
|
73
|
+
const state = makeState();
|
|
74
|
+
const shape = createShape();
|
|
75
|
+
appendShapeBeginFill(shape, 0xff0000);
|
|
76
|
+
appendShapeRectangle(shape, 0, 0, 40, 40);
|
|
77
|
+
appendShapeEndFill(shape);
|
|
78
|
+
const renderProxy = getOrCreateRenderProxy2D(state, shape);
|
|
79
|
+
|
|
80
|
+
const firstCanvas = drawGetEl(state, () => drawDomShape(state, renderProxy));
|
|
81
|
+
const secondCanvas = drawGetEl(state, () => drawDomShape(state, renderProxy));
|
|
82
|
+
|
|
83
|
+
expect(firstCanvas).toBe(secondCanvas);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { setMatrix } from '@flighthq/geometry';
|
|
2
|
+
import { getOrCreateRenderProxy2D } from '@flighthq/render';
|
|
3
|
+
import { createDisplayObject } from '@flighthq/scene2d';
|
|
4
|
+
import { BlendMode } from '@flighthq/types';
|
|
5
|
+
|
|
6
|
+
import { enableDomCssFilterSupport, setDomCssFilter } from './domCSSFilterBinding';
|
|
7
|
+
import { enableDomBlendModeSupport } from './domMaterials';
|
|
8
|
+
import { createDomRenderState, getDomRenderStateRuntime } from './domRenderState';
|
|
9
|
+
import { applyDomStyle, prepareDomElement, setDomRendererElement } from './domStyle';
|
|
10
|
+
|
|
11
|
+
function makeState() {
|
|
12
|
+
const container = document.createElement('div');
|
|
13
|
+
return createDomRenderState(container);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('applyDomStyle', () => {
|
|
17
|
+
it('applies the world transform as a CSS matrix', () => {
|
|
18
|
+
const state = makeState();
|
|
19
|
+
const el = document.createElement('div');
|
|
20
|
+
const obj = createDisplayObject();
|
|
21
|
+
const node = getOrCreateRenderProxy2D(state, obj);
|
|
22
|
+
setMatrix(node.transform2D, 1, 0, 0, 1, 10, 20);
|
|
23
|
+
|
|
24
|
+
applyDomStyle(state, el, node);
|
|
25
|
+
|
|
26
|
+
expect(el.style.transform).toContain('matrix(1,0,0,1,10,20)');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('sets opacity when alpha is less than 1', () => {
|
|
30
|
+
const state = makeState();
|
|
31
|
+
const el = document.createElement('div');
|
|
32
|
+
const obj = createDisplayObject();
|
|
33
|
+
const node = getOrCreateRenderProxy2D(state, obj);
|
|
34
|
+
node.alpha = 0.5;
|
|
35
|
+
|
|
36
|
+
applyDomStyle(state, el, node);
|
|
37
|
+
|
|
38
|
+
expect(el.style.opacity).toBe('0.5');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('clears opacity when alpha is 1', () => {
|
|
42
|
+
const state = makeState();
|
|
43
|
+
const el = document.createElement('div');
|
|
44
|
+
el.style.opacity = '0.5';
|
|
45
|
+
const obj = createDisplayObject();
|
|
46
|
+
const node = getOrCreateRenderProxy2D(state, obj);
|
|
47
|
+
node.alpha = 1;
|
|
48
|
+
|
|
49
|
+
applyDomStyle(state, el, node);
|
|
50
|
+
|
|
51
|
+
expect(el.style.opacity).toBe('');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('sets mixBlendMode for non-default blend mode when blend support is enabled', () => {
|
|
55
|
+
const state = makeState();
|
|
56
|
+
enableDomBlendModeSupport(state);
|
|
57
|
+
const el = document.createElement('div');
|
|
58
|
+
const obj = createDisplayObject();
|
|
59
|
+
const node = getOrCreateRenderProxy2D(state, obj);
|
|
60
|
+
node.blendMode = BlendMode.Multiply;
|
|
61
|
+
|
|
62
|
+
applyDomStyle(state, el, node);
|
|
63
|
+
|
|
64
|
+
expect(el.style.mixBlendMode).toBe('multiply');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('leaves mixBlendMode untouched when blend support is not enabled', () => {
|
|
68
|
+
const state = makeState();
|
|
69
|
+
const el = document.createElement('div');
|
|
70
|
+
const obj = createDisplayObject();
|
|
71
|
+
const node = getOrCreateRenderProxy2D(state, obj);
|
|
72
|
+
node.blendMode = BlendMode.Multiply;
|
|
73
|
+
|
|
74
|
+
applyDomStyle(state, el, node);
|
|
75
|
+
|
|
76
|
+
expect(el.style.mixBlendMode).toBe('');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('applies a bound CSS filter to the element', () => {
|
|
80
|
+
const state = makeState();
|
|
81
|
+
const el = document.createElement('div');
|
|
82
|
+
const obj = createDisplayObject();
|
|
83
|
+
enableDomCssFilterSupport(state);
|
|
84
|
+
setDomCssFilter(state, obj, 'blur(3px)');
|
|
85
|
+
const node = getOrCreateRenderProxy2D(state, obj);
|
|
86
|
+
|
|
87
|
+
applyDomStyle(state, el, node);
|
|
88
|
+
|
|
89
|
+
expect(el.style.filter).toBe('blur(3px)');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('prepareDomElement', () => {
|
|
94
|
+
it('sets position to absolute', () => {
|
|
95
|
+
const el = document.createElement('div');
|
|
96
|
+
prepareDomElement(el);
|
|
97
|
+
expect(el.style.position).toBe('absolute');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('sets left and top to 0', () => {
|
|
101
|
+
const el = document.createElement('div');
|
|
102
|
+
prepareDomElement(el);
|
|
103
|
+
expect(el.style.left).toBe('0px');
|
|
104
|
+
expect(el.style.top).toBe('0px');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('sets transformOrigin to "0 0"', () => {
|
|
108
|
+
const el = document.createElement('div');
|
|
109
|
+
prepareDomElement(el);
|
|
110
|
+
expect(el.style.transformOrigin).toBe('0 0');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('sets pointerEvents to none', () => {
|
|
114
|
+
const el = document.createElement('div');
|
|
115
|
+
prepareDomElement(el);
|
|
116
|
+
expect(el.style.pointerEvents).toBe('none');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('setDomRendererElement', () => {
|
|
121
|
+
it('writes the element to domCurrentElement on the internal state', () => {
|
|
122
|
+
const container = document.createElement('div');
|
|
123
|
+
const state = createDomRenderState(container);
|
|
124
|
+
const el = document.createElement('canvas');
|
|
125
|
+
|
|
126
|
+
setDomRendererElement(state, el);
|
|
127
|
+
|
|
128
|
+
expect(getDomRenderStateRuntime(state).domCurrentElement).toBe(el);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { escapeDomHtmlString } from './domTextHelpers';
|
|
2
|
+
|
|
3
|
+
describe('escapeDomHtmlString', () => {
|
|
4
|
+
it('escapes ampersands', () => {
|
|
5
|
+
expect(escapeDomHtmlString('a&b')).toBe('a&b');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it('escapes greater-than signs', () => {
|
|
9
|
+
expect(escapeDomHtmlString('a>b')).toBe('a>b');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('escapes less-than signs', () => {
|
|
13
|
+
expect(escapeDomHtmlString('a<b')).toBe('a<b');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('escapes multiple special characters in sequence', () => {
|
|
17
|
+
expect(escapeDomHtmlString('<a & b>')).toBe('<a & b>');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('escapes spaces to ', () => {
|
|
21
|
+
expect(escapeDomHtmlString('a b')).toBe('a b');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('returns unchanged string when no special characters', () => {
|
|
25
|
+
expect(escapeDomHtmlString('hello')).toBe('hello');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { getOrCreateRenderProxy2D, registerRenderer } from '@flighthq/render';
|
|
2
|
+
import { createRichText } from '@flighthq/text';
|
|
3
|
+
import { enableTextInput, setTextInputSelection } from '@flighthq/textinput';
|
|
4
|
+
import type { RichText } from '@flighthq/types';
|
|
5
|
+
import { RichTextKind } from '@flighthq/types';
|
|
6
|
+
|
|
7
|
+
import { createDomRenderState, getDomRenderStateRuntime } from './domRenderState';
|
|
8
|
+
import { defaultDomRichTextRenderer, drawDomRichText } from './domRichText';
|
|
9
|
+
import { drawDomTextInputOverlay, enableDomTextInput } from './domTextInput';
|
|
10
|
+
|
|
11
|
+
function makeState() {
|
|
12
|
+
const state = createDomRenderState(document.createElement('div'));
|
|
13
|
+
registerRenderer(state, RichTextKind, defaultDomRichTextRenderer);
|
|
14
|
+
return state;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function makeFocusedInput(text: string): RichText {
|
|
18
|
+
const node = createRichText({ data: { text, width: 100, height: 40 } });
|
|
19
|
+
enableTextInput(node).focused = true;
|
|
20
|
+
return node;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function drawGetEl(state: ReturnType<typeof makeState>, drawFn: () => void): HTMLElement | null {
|
|
24
|
+
getDomRenderStateRuntime(state).domCurrentElement = null;
|
|
25
|
+
drawFn();
|
|
26
|
+
return getDomRenderStateRuntime(state).domCurrentElement;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('drawDomTextInputOverlay', () => {
|
|
30
|
+
it('is the installed overlay function', () => {
|
|
31
|
+
expect(typeof drawDomTextInputOverlay).toBe('function');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('appends a caret for the focused collapsed selection', () => {
|
|
35
|
+
enableDomTextInput();
|
|
36
|
+
const state = makeState();
|
|
37
|
+
const node = makeFocusedInput('hello');
|
|
38
|
+
setTextInputSelection(node, 2, 2);
|
|
39
|
+
const renderProxy = getOrCreateRenderProxy2D(state, node);
|
|
40
|
+
|
|
41
|
+
const div = drawGetEl(state, () => drawDomRichText(state, renderProxy));
|
|
42
|
+
expect(div!.innerHTML).toContain('background:#000000');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('appends selection rectangles for the focused expanded selection', () => {
|
|
46
|
+
enableDomTextInput();
|
|
47
|
+
const state = makeState();
|
|
48
|
+
const node = makeFocusedInput('hello');
|
|
49
|
+
setTextInputSelection(node, 1, 4);
|
|
50
|
+
const renderProxy = getOrCreateRenderProxy2D(state, node);
|
|
51
|
+
|
|
52
|
+
const div = drawGetEl(state, () => drawDomRichText(state, renderProxy));
|
|
53
|
+
expect(div!.innerHTML).toContain('background:#0078d7');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('enableDomTextInput', () => {
|
|
58
|
+
it('installs the overlay without throwing', () => {
|
|
59
|
+
expect(() => enableDomTextInput()).not.toThrow();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { registerRenderer } from '@flighthq/render';
|
|
2
|
+
import { getOrCreateRenderProxy2D } from '@flighthq/render';
|
|
3
|
+
import { createTextLabel } from '@flighthq/text';
|
|
4
|
+
import { TextLabelKind } from '@flighthq/types';
|
|
5
|
+
|
|
6
|
+
import { createDomRenderState, getDomRenderStateRuntime } from './domRenderState';
|
|
7
|
+
import { defaultDomTextLabelRenderer, drawDomTextLabel } from './domTextLabel';
|
|
8
|
+
|
|
9
|
+
function makeState() {
|
|
10
|
+
const container = document.createElement('div');
|
|
11
|
+
const state = createDomRenderState(container);
|
|
12
|
+
registerRenderer(state, TextLabelKind, defaultDomTextLabelRenderer);
|
|
13
|
+
return state;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function drawGetEl(state: ReturnType<typeof makeState>, drawFn: () => void): HTMLElement | null {
|
|
17
|
+
getDomRenderStateRuntime(state).domCurrentElement = null;
|
|
18
|
+
drawFn();
|
|
19
|
+
return getDomRenderStateRuntime(state).domCurrentElement;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('defaultDomTextLabelRenderer', () => {
|
|
23
|
+
it('has submit, and createData', () => {
|
|
24
|
+
expect(typeof defaultDomTextLabelRenderer.submit).toBe('function');
|
|
25
|
+
expect(typeof defaultDomTextLabelRenderer.createData).toBe('function');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('drawDomTextLabel', () => {
|
|
30
|
+
it('does not throw when text is empty', () => {
|
|
31
|
+
const state = makeState();
|
|
32
|
+
const node = createTextLabel();
|
|
33
|
+
node.data.text = '';
|
|
34
|
+
const renderProxy = getOrCreateRenderProxy2D(state, node);
|
|
35
|
+
expect(() => drawDomTextLabel(state, renderProxy)).not.toThrow();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('produces no element when text is empty', () => {
|
|
39
|
+
const state = makeState();
|
|
40
|
+
const node = createTextLabel();
|
|
41
|
+
node.data.text = '';
|
|
42
|
+
const renderProxy = getOrCreateRenderProxy2D(state, node);
|
|
43
|
+
|
|
44
|
+
const el = drawGetEl(state, () => drawDomTextLabel(state, renderProxy));
|
|
45
|
+
|
|
46
|
+
expect(el).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('produces a div when text is non-empty', () => {
|
|
50
|
+
const state = makeState();
|
|
51
|
+
const node = createTextLabel();
|
|
52
|
+
node.data.text = 'hello';
|
|
53
|
+
const renderProxy = getOrCreateRenderProxy2D(state, node);
|
|
54
|
+
|
|
55
|
+
const el = drawGetEl(state, () => drawDomTextLabel(state, renderProxy));
|
|
56
|
+
|
|
57
|
+
expect(el).not.toBeNull();
|
|
58
|
+
expect(el!.tagName).toBe('DIV');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('sets overflow:hidden on the div', () => {
|
|
62
|
+
const state = makeState();
|
|
63
|
+
const node = createTextLabel();
|
|
64
|
+
node.data.text = 'hello';
|
|
65
|
+
const renderProxy = getOrCreateRenderProxy2D(state, node);
|
|
66
|
+
|
|
67
|
+
const div = drawGetEl(state, () => drawDomTextLabel(state, renderProxy))!;
|
|
68
|
+
expect(div.style.overflow).toBe('hidden');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('includes the text content in innerHtml', () => {
|
|
72
|
+
const state = makeState();
|
|
73
|
+
const node = createTextLabel();
|
|
74
|
+
node.data.text = 'world';
|
|
75
|
+
const renderProxy = getOrCreateRenderProxy2D(state, node);
|
|
76
|
+
|
|
77
|
+
const div = drawGetEl(state, () => drawDomTextLabel(state, renderProxy))!;
|
|
78
|
+
expect(div.innerHTML).toContain('world');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('reuses the same div across multiple draws', () => {
|
|
82
|
+
const state = makeState();
|
|
83
|
+
const node = createTextLabel();
|
|
84
|
+
node.data.text = 'hello';
|
|
85
|
+
const renderProxy = getOrCreateRenderProxy2D(state, node);
|
|
86
|
+
|
|
87
|
+
const firstDiv = drawGetEl(state, () => drawDomTextLabel(state, renderProxy));
|
|
88
|
+
const secondDiv = drawGetEl(state, () => drawDomTextLabel(state, renderProxy));
|
|
89
|
+
|
|
90
|
+
expect(firstDiv).toBe(secondDiv);
|
|
91
|
+
});
|
|
92
|
+
});
|