@lightningtv/solid 3.0.0-19 → 3.0.0-20
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/src/primitives/Lazy.d.ts +1 -0
- package/dist/src/primitives/Lazy.jsx +14 -1
- package/dist/src/primitives/Lazy.jsx.map +1 -1
- package/dist/src/primitives/Virtual.jsx +7 -1
- package/dist/src/primitives/Virtual.jsx.map +1 -1
- package/dist/src/primitives/announcer/announcer.d.ts +1 -0
- package/dist/src/primitives/announcer/announcer.js +4 -3
- package/dist/src/primitives/announcer/announcer.js.map +1 -1
- package/dist/src/primitives/announcer/speech.d.ts +1 -1
- package/dist/src/primitives/announcer/speech.js +98 -8
- package/dist/src/primitives/announcer/speech.js.map +1 -1
- package/dist/src/primitives/createTag.d.ts +8 -0
- package/dist/src/primitives/createTag.jsx +20 -0
- package/dist/src/primitives/createTag.jsx.map +1 -0
- package/dist/src/primitives/index.d.ts +2 -0
- package/dist/src/primitives/index.js +2 -0
- package/dist/src/primitives/index.js.map +1 -1
- package/dist/src/primitives/useMouse.d.ts +18 -1
- package/dist/src/primitives/useMouse.js +142 -59
- package/dist/src/primitives/useMouse.js.map +1 -1
- package/dist/src/primitives/utils/createBlurredImage.d.ts +56 -0
- package/dist/src/primitives/utils/createBlurredImage.js +223 -0
- package/dist/src/primitives/utils/createBlurredImage.js.map +1 -0
- package/dist/src/primitives/utils/handleNavigation.js +7 -13
- package/dist/src/primitives/utils/handleNavigation.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -5
- package/src/primitives/Lazy.tsx +15 -3
- package/src/primitives/Virtual.tsx +8 -1
- package/src/primitives/announcer/announcer.ts +10 -3
- package/src/primitives/announcer/speech.ts +113 -6
- package/src/primitives/createTag.tsx +31 -0
- package/src/primitives/index.ts +2 -0
- package/src/primitives/useMouse.ts +253 -81
- package/src/primitives/utils/createBlurredImage.ts +366 -0
- package/src/primitives/utils/handleNavigation.ts +9 -14
|
@@ -12,6 +12,11 @@ export interface SeriesResult {
|
|
|
12
12
|
cancel: () => void;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
// Aria label
|
|
16
|
+
type AriaLabel = { text: string; lang: string };
|
|
17
|
+
const ARIA_PARENT_ID = 'aria-parent';
|
|
18
|
+
let ariaLabelPhrases: AriaLabel[] = [];
|
|
19
|
+
|
|
15
20
|
/* global SpeechSynthesisErrorEvent */
|
|
16
21
|
function flattenStrings(series: SpeechType[] = []): SpeechType[] {
|
|
17
22
|
const flattenedSeries = [];
|
|
@@ -40,6 +45,82 @@ function delay(pause: number) {
|
|
|
40
45
|
});
|
|
41
46
|
}
|
|
42
47
|
|
|
48
|
+
/**
|
|
49
|
+
* @description This function is called at the end of the speak series
|
|
50
|
+
* @param Phrase is an object containing the text and the language
|
|
51
|
+
*/
|
|
52
|
+
function addChildrenToAriaDiv(phrase: AriaLabel) {
|
|
53
|
+
if (phrase?.text?.trim().length === 0) return;
|
|
54
|
+
ariaLabelPhrases.push(phrase);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @description This function is triggered finally when the speak series is finished and we are to speak the aria labels
|
|
59
|
+
*/
|
|
60
|
+
function focusElementForAria() {
|
|
61
|
+
const element = createAriaElement();
|
|
62
|
+
|
|
63
|
+
if (!element) {
|
|
64
|
+
console.error(`ARIA div not found: ${ARIA_PARENT_ID}`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const object of ariaLabelPhrases) {
|
|
69
|
+
const span = document.createElement('span');
|
|
70
|
+
|
|
71
|
+
// TODO: Not sure LG or Samsung support lang attribute on span or switching language
|
|
72
|
+
span.setAttribute('lang', object.lang);
|
|
73
|
+
span.setAttribute('aria-label', object.text);
|
|
74
|
+
element.appendChild(span);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Cleanup
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
ariaLabelPhrases = [];
|
|
80
|
+
cleanAriaLabelParent();
|
|
81
|
+
focusCanvas();
|
|
82
|
+
}, 100);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @description Clean the aria label parent after speaking
|
|
87
|
+
*/
|
|
88
|
+
function cleanAriaLabelParent(): void {
|
|
89
|
+
const parentTag = document.getElementById(ARIA_PARENT_ID);
|
|
90
|
+
if (parentTag) {
|
|
91
|
+
while (parentTag.firstChild) {
|
|
92
|
+
parentTag.removeChild(parentTag.firstChild);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @description Focus the canvas element
|
|
99
|
+
*/
|
|
100
|
+
function focusCanvas(): void {
|
|
101
|
+
const canvas = document.getElementById('app')?.firstChild as HTMLElement;
|
|
102
|
+
canvas?.focus();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @description Create the aria element in the DOM if it doesn't exist
|
|
107
|
+
* @private For xbox, we may need to create a different element each time we wanna use aria
|
|
108
|
+
*/
|
|
109
|
+
function createAriaElement(): HTMLDivElement | HTMLElement {
|
|
110
|
+
const aria_container = document.getElementById(ARIA_PARENT_ID);
|
|
111
|
+
|
|
112
|
+
if (!aria_container) {
|
|
113
|
+
const element = document.createElement('div');
|
|
114
|
+
element.setAttribute('id', ARIA_PARENT_ID);
|
|
115
|
+
element.setAttribute('aria-live', 'assertive');
|
|
116
|
+
element.setAttribute('tabindex', '0');
|
|
117
|
+
document.body.appendChild(element);
|
|
118
|
+
return element;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return aria_container;
|
|
122
|
+
}
|
|
123
|
+
|
|
43
124
|
/**
|
|
44
125
|
* Speak a string
|
|
45
126
|
*
|
|
@@ -82,6 +163,7 @@ function speak(
|
|
|
82
163
|
|
|
83
164
|
function speakSeries(
|
|
84
165
|
series: SpeechType,
|
|
166
|
+
aria: boolean,
|
|
85
167
|
lang: string,
|
|
86
168
|
voice?: string,
|
|
87
169
|
root = true,
|
|
@@ -118,7 +200,8 @@ function speakSeries(
|
|
|
118
200
|
|
|
119
201
|
while (active && retriesLeft > 0) {
|
|
120
202
|
try {
|
|
121
|
-
|
|
203
|
+
if (aria) addChildrenToAriaDiv({ text: phrase, lang });
|
|
204
|
+
else await speak(phrase, utterances, lang, voice);
|
|
122
205
|
retriesLeft = 0; // Exit retry loop on success
|
|
123
206
|
} catch (e) {
|
|
124
207
|
if (e instanceof SpeechSynthesisErrorEvent) {
|
|
@@ -152,8 +235,12 @@ function speakSeries(
|
|
|
152
235
|
|
|
153
236
|
while (active && retriesLeft > 0) {
|
|
154
237
|
try {
|
|
155
|
-
|
|
156
|
-
|
|
238
|
+
if (text) {
|
|
239
|
+
if (aria) addChildrenToAriaDiv({ text, lang: objectLang });
|
|
240
|
+
else
|
|
241
|
+
await speak(text, utterances, objectLang, objectVoice?.name);
|
|
242
|
+
retriesLeft = 0; // Exit retry loop on success
|
|
243
|
+
}
|
|
157
244
|
} catch (e) {
|
|
158
245
|
if (e instanceof SpeechSynthesisErrorEvent) {
|
|
159
246
|
if (e.error === 'network') {
|
|
@@ -178,18 +265,22 @@ function speakSeries(
|
|
|
178
265
|
}
|
|
179
266
|
} else if (typeof phrase === 'function') {
|
|
180
267
|
// Handle functions
|
|
181
|
-
const seriesResult = speakSeries(phrase(), lang, voice, false);
|
|
268
|
+
const seriesResult = speakSeries(phrase(), aria, lang, voice, false);
|
|
182
269
|
nestedSeriesResults.push(seriesResult);
|
|
183
270
|
await seriesResult.series;
|
|
184
271
|
} else if (Array.isArray(phrase)) {
|
|
185
272
|
// Handle nested arrays
|
|
186
|
-
const seriesResult = speakSeries(phrase, lang, voice, false);
|
|
273
|
+
const seriesResult = speakSeries(phrase, aria, lang, voice, false);
|
|
187
274
|
nestedSeriesResults.push(seriesResult);
|
|
188
275
|
await seriesResult.series;
|
|
189
276
|
}
|
|
190
277
|
}
|
|
191
278
|
} finally {
|
|
192
279
|
active = false;
|
|
280
|
+
// Call completion logic only for the original (root) series
|
|
281
|
+
if (root && aria) {
|
|
282
|
+
focusElementForAria();
|
|
283
|
+
}
|
|
193
284
|
}
|
|
194
285
|
})();
|
|
195
286
|
|
|
@@ -205,7 +296,21 @@ function speakSeries(
|
|
|
205
296
|
if (!active) {
|
|
206
297
|
return;
|
|
207
298
|
}
|
|
299
|
+
|
|
208
300
|
if (root) {
|
|
301
|
+
if (aria) {
|
|
302
|
+
const element = createAriaElement();
|
|
303
|
+
|
|
304
|
+
if (element) {
|
|
305
|
+
ariaLabelPhrases = [];
|
|
306
|
+
cleanAriaLabelParent();
|
|
307
|
+
element.focus();
|
|
308
|
+
focusCanvas();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
209
314
|
synth.cancel(); // Cancel all ongoing speech
|
|
210
315
|
}
|
|
211
316
|
nestedSeriesResults.forEach((nestedSeriesResult) => {
|
|
@@ -215,13 +320,15 @@ function speakSeries(
|
|
|
215
320
|
},
|
|
216
321
|
};
|
|
217
322
|
}
|
|
323
|
+
|
|
218
324
|
let currentSeries: SeriesResult | undefined;
|
|
219
325
|
export default function (
|
|
220
326
|
toSpeak: SpeechType,
|
|
327
|
+
aria: boolean,
|
|
221
328
|
lang: string = 'en-US',
|
|
222
329
|
voice?: string,
|
|
223
330
|
) {
|
|
224
331
|
currentSeries && currentSeries.cancel();
|
|
225
|
-
currentSeries = speakSeries(toSpeak, lang, voice);
|
|
332
|
+
currentSeries = speakSeries(toSpeak, aria, lang, voice);
|
|
226
333
|
return currentSeries;
|
|
227
334
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as s from 'solid-js'
|
|
2
|
+
import * as lng from '@lightningtv/solid'
|
|
3
|
+
|
|
4
|
+
interface Destroyable {
|
|
5
|
+
(props: lng.NodeProps): s.JSX.Element;
|
|
6
|
+
destroy: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createTag(children: s.JSX.Element): Destroyable {
|
|
10
|
+
const [texture, setTexture] = s.createSignal<lng.Texture | null | undefined>(null);
|
|
11
|
+
const Tag = <view
|
|
12
|
+
display='flex'
|
|
13
|
+
onLayout={(n) => {
|
|
14
|
+
if (n.preFlexwidth && n.width !== n.preFlexwidth) {
|
|
15
|
+
n.rtt = true;
|
|
16
|
+
setTimeout(() => setTexture(n.texture), 1);
|
|
17
|
+
}
|
|
18
|
+
}}
|
|
19
|
+
parent={lng.rootNode} children={children}
|
|
20
|
+
textureOptions={{
|
|
21
|
+
preventCleanup: true
|
|
22
|
+
}} /> as any as lng.ElementNode
|
|
23
|
+
Tag.render(false);
|
|
24
|
+
|
|
25
|
+
const TagComponent = (props: lng.NodeProps) => {
|
|
26
|
+
return <view color={0xffffffff} autosize {...props} texture={texture()} />;
|
|
27
|
+
};
|
|
28
|
+
TagComponent.destroy = () => Tag.destroy();
|
|
29
|
+
|
|
30
|
+
return TagComponent;
|
|
31
|
+
}
|
package/src/primitives/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ export * from './KeepAlive.jsx';
|
|
|
21
21
|
export * from './VirtualGrid.jsx';
|
|
22
22
|
export * from './Virtual.jsx';
|
|
23
23
|
export * from './utils/withScrolling.js';
|
|
24
|
+
export * from './createTag.jsx';
|
|
24
25
|
export {
|
|
25
26
|
type AnyFunction,
|
|
26
27
|
chainFunctions,
|
|
@@ -28,6 +29,7 @@ export {
|
|
|
28
29
|
} from './utils/chainFunctions.js';
|
|
29
30
|
export * from './utils/handleNavigation.js';
|
|
30
31
|
export { createSpriteMap, type SpriteDef } from './utils/createSpriteMap.js';
|
|
32
|
+
export { createBlurredImage } from './utils/createBlurredImage.js';
|
|
31
33
|
|
|
32
34
|
export type * from './types.js';
|
|
33
35
|
export type { KeyHandler } from '@lightningtv/core/focusManager';
|
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
import type { ElementText, TextNode } from '@lightningtv/core';
|
|
2
2
|
import {
|
|
3
|
+
Config,
|
|
3
4
|
ElementNode,
|
|
4
5
|
activeElement,
|
|
5
6
|
isElementNode,
|
|
7
|
+
isFunc,
|
|
6
8
|
isTextNode,
|
|
7
9
|
rootNode,
|
|
8
|
-
Config,
|
|
9
|
-
isFunc,
|
|
10
10
|
} from '@lightningtv/solid';
|
|
11
11
|
import { makeEventListener } from '@solid-primitives/event-listener';
|
|
12
12
|
import { useMousePosition } from '@solid-primitives/mouse';
|
|
13
13
|
import { createScheduled, throttle } from '@solid-primitives/scheduled';
|
|
14
|
-
import { createEffect } from 'solid-js';
|
|
14
|
+
import { createEffect, getOwner, runWithOwner } from 'solid-js';
|
|
15
|
+
|
|
16
|
+
type CustomState = `$${string}`;
|
|
17
|
+
|
|
18
|
+
type RenderableNode = ElementNode | ElementText | TextNode;
|
|
19
|
+
|
|
20
|
+
interface MouseStateOptions {
|
|
21
|
+
hoverState: CustomState;
|
|
22
|
+
pressedState: CustomState;
|
|
23
|
+
pressedStateDuration?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type UseMouseOptions =
|
|
27
|
+
| { customStates: MouseStateOptions }
|
|
28
|
+
| { customStates: undefined };
|
|
15
29
|
|
|
16
30
|
declare module '@lightningtv/core' {
|
|
17
31
|
interface ElementNode {
|
|
@@ -24,6 +38,29 @@ declare module '@lightningtv/core' {
|
|
|
24
38
|
}
|
|
25
39
|
}
|
|
26
40
|
|
|
41
|
+
const DEFAULT_PRESSED_STATE_DURATION = 150;
|
|
42
|
+
|
|
43
|
+
export function addCustomStateToElement(
|
|
44
|
+
element: RenderableNode,
|
|
45
|
+
state: CustomState,
|
|
46
|
+
): void {
|
|
47
|
+
element.states?.add(state);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function removeCustomStateFromElement(
|
|
51
|
+
element: RenderableNode,
|
|
52
|
+
state: CustomState,
|
|
53
|
+
): void {
|
|
54
|
+
element?.states?.remove(state);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function hasCustomState(
|
|
58
|
+
element: RenderableNode,
|
|
59
|
+
state: CustomState,
|
|
60
|
+
): boolean {
|
|
61
|
+
return element.states?.has(state);
|
|
62
|
+
}
|
|
63
|
+
|
|
27
64
|
function createKeyboardEvent(
|
|
28
65
|
key: string,
|
|
29
66
|
keyCode: number,
|
|
@@ -41,7 +78,7 @@ function createKeyboardEvent(
|
|
|
41
78
|
});
|
|
42
79
|
}
|
|
43
80
|
|
|
44
|
-
let scrollTimeout:
|
|
81
|
+
let scrollTimeout: ReturnType<typeof setTimeout>;
|
|
45
82
|
const handleScroll = throttle((e: WheelEvent): void => {
|
|
46
83
|
const deltaY = e.deltaY;
|
|
47
84
|
if (deltaY < 0) {
|
|
@@ -59,9 +96,38 @@ const handleScroll = throttle((e: WheelEvent): void => {
|
|
|
59
96
|
}, 250);
|
|
60
97
|
}, 250);
|
|
61
98
|
|
|
62
|
-
|
|
99
|
+
function findElementWithCustomState<TApp extends ElementNode>(
|
|
100
|
+
myApp: TApp,
|
|
101
|
+
x: number,
|
|
102
|
+
y: number,
|
|
103
|
+
customState: CustomState,
|
|
104
|
+
): ElementNode | undefined {
|
|
105
|
+
const result = getChildrenByPosition(myApp, x, y).filter((el) =>
|
|
106
|
+
hasCustomState(el, customState),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (result.length === 0) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let element: ElementNode | undefined = result[result.length - 1];
|
|
114
|
+
|
|
115
|
+
while (element) {
|
|
116
|
+
const elmParent = element.parent;
|
|
117
|
+
if (elmParent?.forwardStates && hasCustomState(elmParent, customState)) {
|
|
118
|
+
element = elmParent;
|
|
119
|
+
} else {
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return element;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function findElementByActiveElement(e: MouseEvent): ElementNode | null {
|
|
63
128
|
const active = activeElement();
|
|
64
129
|
const precision = Config.rendererOptions?.deviceLogicalPixelRatio || 1;
|
|
130
|
+
|
|
65
131
|
if (
|
|
66
132
|
active instanceof ElementNode &&
|
|
67
133
|
testCollision(
|
|
@@ -73,38 +139,95 @@ const handleClick = (e: MouseEvent): void => {
|
|
|
73
139
|
(active.height || 0) * precision,
|
|
74
140
|
)
|
|
75
141
|
) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
142
|
+
return active;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let parent = active?.parent;
|
|
146
|
+
while (parent) {
|
|
147
|
+
if (
|
|
148
|
+
isFunc(parent.onMouseClick) &&
|
|
149
|
+
active &&
|
|
150
|
+
testCollision(
|
|
151
|
+
e.clientX,
|
|
152
|
+
e.clientY,
|
|
153
|
+
((parent.lng.absX as number) || 0) * precision,
|
|
154
|
+
((parent.lng.absY as number) || 0) * precision,
|
|
155
|
+
(parent.width || 0) * precision,
|
|
156
|
+
(parent.height || 0) * precision,
|
|
157
|
+
)
|
|
158
|
+
) {
|
|
159
|
+
return parent;
|
|
79
160
|
}
|
|
161
|
+
parent = parent.parent;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function applyPressedState(
|
|
168
|
+
element: ElementNode,
|
|
169
|
+
pressedState: CustomState,
|
|
170
|
+
pressedStateDuration: number = DEFAULT_PRESSED_STATE_DURATION,
|
|
171
|
+
): void {
|
|
172
|
+
addCustomStateToElement(element, pressedState);
|
|
173
|
+
setTimeout(() => {
|
|
174
|
+
removeCustomStateFromElement(element, pressedState);
|
|
175
|
+
}, pressedStateDuration);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function handleElementClick(
|
|
179
|
+
clickedElement: ElementNode,
|
|
180
|
+
e: MouseEvent,
|
|
181
|
+
customStates?: MouseStateOptions,
|
|
182
|
+
): void {
|
|
183
|
+
if (customStates?.pressedState) {
|
|
184
|
+
applyPressedState(
|
|
185
|
+
clickedElement,
|
|
186
|
+
customStates.pressedState,
|
|
187
|
+
customStates.pressedStateDuration,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (isFunc(clickedElement.onMouseClick)) {
|
|
192
|
+
clickedElement.onMouseClick(e, clickedElement);
|
|
193
|
+
return;
|
|
194
|
+
} else if (isFunc(clickedElement.onEnter)) {
|
|
195
|
+
clickedElement.onEnter();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
80
198
|
|
|
199
|
+
clickedElement.setFocus();
|
|
200
|
+
setTimeout(() => {
|
|
81
201
|
document.dispatchEvent(createKeyboardEvent('Enter', 13));
|
|
82
202
|
setTimeout(
|
|
83
203
|
() =>
|
|
84
204
|
document.body.dispatchEvent(createKeyboardEvent('Enter', 13, 'keyup')),
|
|
85
205
|
1,
|
|
86
206
|
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
207
|
+
}, 1);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function createHandleClick<TApp extends ElementNode>(
|
|
211
|
+
myApp: TApp,
|
|
212
|
+
customStates?: MouseStateOptions,
|
|
213
|
+
) {
|
|
214
|
+
return (e: MouseEvent): void => {
|
|
215
|
+
const clickedElement = customStates
|
|
216
|
+
? findElementWithCustomState(
|
|
217
|
+
myApp,
|
|
93
218
|
e.clientX,
|
|
94
219
|
e.clientY,
|
|
95
|
-
|
|
96
|
-
((parent.lng.absY as number) || 0) * precision,
|
|
97
|
-
(parent.width || 0) * precision,
|
|
98
|
-
(parent.height || 0) * precision,
|
|
220
|
+
customStates.hoverState,
|
|
99
221
|
)
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
parent = parent.parent;
|
|
222
|
+
: findElementByActiveElement(e);
|
|
223
|
+
|
|
224
|
+
if (!clickedElement) {
|
|
225
|
+
return;
|
|
105
226
|
}
|
|
106
|
-
|
|
107
|
-
|
|
227
|
+
|
|
228
|
+
handleElementClick(clickedElement, e, customStates);
|
|
229
|
+
};
|
|
230
|
+
}
|
|
108
231
|
|
|
109
232
|
function testCollision(
|
|
110
233
|
px: number,
|
|
@@ -117,106 +240,155 @@ function testCollision(
|
|
|
117
240
|
return px >= cx && px <= cx + cw && py >= cy && py <= cy + ch;
|
|
118
241
|
}
|
|
119
242
|
|
|
120
|
-
function
|
|
121
|
-
node: ElementNode,
|
|
243
|
+
function isNodeAtPosition(
|
|
244
|
+
node: ElementNode | ElementText | TextNode,
|
|
122
245
|
x: number,
|
|
123
246
|
y: number,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
247
|
+
precision: number,
|
|
248
|
+
): node is ElementNode {
|
|
249
|
+
if (!isElementNode(node)) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
node.alpha !== 0 &&
|
|
255
|
+
!node.skipFocus &&
|
|
256
|
+
testCollision(
|
|
257
|
+
x,
|
|
258
|
+
y,
|
|
259
|
+
((node.lng.absX as number) || 0) * precision,
|
|
260
|
+
((node.lng.absY as number) || 0) * precision,
|
|
261
|
+
(node.width || 0) * precision,
|
|
262
|
+
(node.height || 0) * precision,
|
|
263
|
+
)
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function findHighestZIndexNode(nodes: ElementNode[]): ElementNode | undefined {
|
|
268
|
+
if (nodes.length === 0) {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (nodes.length === 1) {
|
|
273
|
+
return nodes[0];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let maxZIndex = -1;
|
|
277
|
+
let highestNode: ElementNode | undefined = undefined;
|
|
278
|
+
|
|
279
|
+
for (const node of nodes) {
|
|
280
|
+
const zIndex = node.zIndex ?? -1;
|
|
281
|
+
if (zIndex >= maxZIndex) {
|
|
282
|
+
maxZIndex = zIndex;
|
|
283
|
+
highestNode = node;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
127
286
|
|
|
287
|
+
return highestNode;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function getChildrenByPosition<TElement extends ElementNode = ElementNode>(
|
|
291
|
+
node: TElement,
|
|
292
|
+
x: number,
|
|
293
|
+
y: number,
|
|
294
|
+
): TElement[] {
|
|
295
|
+
const result: TElement[] = [];
|
|
296
|
+
const precision = Config.rendererOptions?.deviceLogicalPixelRatio || 1;
|
|
128
297
|
// Queue for BFS
|
|
298
|
+
|
|
129
299
|
let queue: (ElementNode | ElementText | TextNode)[] = [node];
|
|
130
300
|
|
|
131
301
|
while (queue.length > 0) {
|
|
132
302
|
// Process nodes at the current level
|
|
133
|
-
const currentLevelNodes
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
isElementNode(currentNode) &&
|
|
138
|
-
currentNode.alpha !== 0 &&
|
|
139
|
-
!currentNode.skipFocus &&
|
|
140
|
-
testCollision(
|
|
141
|
-
x,
|
|
142
|
-
y,
|
|
143
|
-
((currentNode.lng.absX as number) || 0) * precision,
|
|
144
|
-
((currentNode.lng.absY as number) || 0) * precision,
|
|
145
|
-
(currentNode.width || 0) * precision,
|
|
146
|
-
(currentNode.height || 0) * precision,
|
|
147
|
-
)
|
|
148
|
-
) {
|
|
149
|
-
currentLevelNodes.push(currentNode);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
303
|
+
const currentLevelNodes = queue.filter((currentNode) =>
|
|
304
|
+
isNodeAtPosition(currentNode, x, y, precision),
|
|
305
|
+
);
|
|
152
306
|
|
|
153
|
-
|
|
154
|
-
if (size === 0) {
|
|
307
|
+
if (currentLevelNodes.length === 0) {
|
|
155
308
|
break;
|
|
156
309
|
}
|
|
157
310
|
|
|
158
|
-
|
|
159
|
-
if (size === 1) {
|
|
160
|
-
highestZIndexNode = currentLevelNodes[0];
|
|
161
|
-
} else {
|
|
162
|
-
let maxZIndex = -1;
|
|
163
|
-
|
|
164
|
-
for (const node of currentLevelNodes) {
|
|
165
|
-
const zIndex = node.zIndex ?? -1;
|
|
166
|
-
if (zIndex > maxZIndex) {
|
|
167
|
-
maxZIndex = zIndex;
|
|
168
|
-
highestZIndexNode = node;
|
|
169
|
-
} else if (zIndex === maxZIndex) {
|
|
170
|
-
highestZIndexNode = node;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
311
|
+
const highestZIndexNode = findHighestZIndexNode(currentLevelNodes);
|
|
174
312
|
|
|
175
|
-
if (highestZIndexNode
|
|
176
|
-
|
|
177
|
-
queue = highestZIndexNode.children;
|
|
178
|
-
} else {
|
|
179
|
-
queue = [];
|
|
313
|
+
if (!highestZIndexNode || isTextNode(highestZIndexNode)) {
|
|
314
|
+
break;
|
|
180
315
|
}
|
|
316
|
+
|
|
317
|
+
result.push(highestZIndexNode as TElement);
|
|
318
|
+
queue = highestZIndexNode.children;
|
|
181
319
|
}
|
|
182
320
|
|
|
183
321
|
return result;
|
|
184
322
|
}
|
|
185
323
|
|
|
186
|
-
export function useMouse(
|
|
187
|
-
myApp:
|
|
324
|
+
export function useMouse<TApp extends ElementNode = ElementNode>(
|
|
325
|
+
myApp: TApp = rootNode as TApp,
|
|
188
326
|
throttleBy: number = 100,
|
|
327
|
+
options?: UseMouseOptions,
|
|
189
328
|
): void {
|
|
190
329
|
const pos = useMousePosition();
|
|
191
330
|
const scheduled = createScheduled((fn) => throttle(fn, throttleBy));
|
|
331
|
+
let previousElement: ElementNode | null = null;
|
|
332
|
+
const customStates = options?.customStates;
|
|
333
|
+
const hoverState = customStates?.hoverState;
|
|
334
|
+
const handleClick = createHandleClick(myApp, customStates);
|
|
335
|
+
const owner = getOwner();
|
|
336
|
+
const handleClickContext = (e: MouseEvent) => {
|
|
337
|
+
runWithOwner(owner, () => handleClick(e));
|
|
338
|
+
};
|
|
339
|
+
|
|
192
340
|
makeEventListener(window, 'wheel', handleScroll);
|
|
193
|
-
makeEventListener(window, 'click',
|
|
341
|
+
makeEventListener(window, 'click', handleClickContext);
|
|
194
342
|
createEffect(() => {
|
|
195
343
|
if (scheduled()) {
|
|
196
344
|
const result = getChildrenByPosition(myApp, pos.x, pos.y).filter(
|
|
197
|
-
(el) =>
|
|
345
|
+
(el) =>
|
|
346
|
+
!!(
|
|
347
|
+
el.onEnter ||
|
|
348
|
+
el.onMouseClick ||
|
|
349
|
+
el.onFocus ||
|
|
350
|
+
el[Config.focusStateKey] ||
|
|
351
|
+
(hoverState ? el[hoverState] : false)
|
|
352
|
+
),
|
|
198
353
|
);
|
|
199
354
|
|
|
200
355
|
if (result.length) {
|
|
201
|
-
let activeElm = result[result.length - 1];
|
|
356
|
+
let activeElm: ElementNode | undefined = result[result.length - 1];
|
|
202
357
|
|
|
203
358
|
while (activeElm) {
|
|
204
359
|
const elmParent = activeElm.parent;
|
|
205
360
|
if (elmParent?.forwardStates) {
|
|
206
|
-
activeElm =
|
|
361
|
+
activeElm = elmParent;
|
|
207
362
|
} else {
|
|
208
363
|
break;
|
|
209
364
|
}
|
|
210
365
|
}
|
|
211
366
|
|
|
367
|
+
if (!activeElm) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
212
371
|
// Update Row & Column Selected property
|
|
213
|
-
const activeElmParent = activeElm
|
|
214
|
-
if (
|
|
372
|
+
const activeElmParent = activeElm.parent;
|
|
373
|
+
if (activeElmParent?.selected !== undefined) {
|
|
215
374
|
activeElmParent.selected =
|
|
216
375
|
activeElmParent.children.indexOf(activeElm);
|
|
217
376
|
}
|
|
218
377
|
|
|
219
|
-
activeElm
|
|
378
|
+
if (previousElement && previousElement !== activeElm && hoverState) {
|
|
379
|
+
removeCustomStateFromElement(previousElement, hoverState);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (hoverState) {
|
|
383
|
+
addCustomStateToElement(activeElm, hoverState);
|
|
384
|
+
} else {
|
|
385
|
+
activeElm.setFocus();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
previousElement = activeElm;
|
|
389
|
+
} else if (previousElement && hoverState) {
|
|
390
|
+
removeCustomStateFromElement(previousElement, hoverState);
|
|
391
|
+
previousElement = null;
|
|
220
392
|
}
|
|
221
393
|
}
|
|
222
394
|
});
|