@lightningtv/solid 2.12.1 → 2.12.3

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.
@@ -1,20 +1,35 @@
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 {
32
+ onEnter?: () => void;
18
33
  /** function to be called on mouse click */
19
34
  onMouseClick?: (
20
35
  this: ElementNode,
@@ -24,6 +39,29 @@ declare module '@lightningtv/core' {
24
39
  }
25
40
  }
26
41
 
42
+ const DEFAULT_PRESSED_STATE_DURATION = 150;
43
+
44
+ export function addCustomStateToElement(
45
+ element: RenderableNode,
46
+ state: CustomState,
47
+ ): void {
48
+ element.states?.add(state);
49
+ }
50
+
51
+ export function removeCustomStateFromElement(
52
+ element: RenderableNode,
53
+ state: CustomState,
54
+ ): void {
55
+ element?.states?.remove(state);
56
+ }
57
+
58
+ export function hasCustomState(
59
+ element: RenderableNode,
60
+ state: CustomState,
61
+ ): boolean {
62
+ return element.states?.has(state);
63
+ }
64
+
27
65
  function createKeyboardEvent(
28
66
  key: string,
29
67
  keyCode: number,
@@ -41,7 +79,7 @@ function createKeyboardEvent(
41
79
  });
42
80
  }
43
81
 
44
- let scrollTimeout: number;
82
+ let scrollTimeout: ReturnType<typeof setTimeout>;
45
83
  const handleScroll = throttle((e: WheelEvent): void => {
46
84
  const deltaY = e.deltaY;
47
85
  if (deltaY < 0) {
@@ -59,9 +97,38 @@ const handleScroll = throttle((e: WheelEvent): void => {
59
97
  }, 250);
60
98
  }, 250);
61
99
 
62
- const handleClick = (e: MouseEvent): void => {
100
+ function findElementWithCustomState<TApp extends ElementNode>(
101
+ myApp: TApp,
102
+ x: number,
103
+ y: number,
104
+ customState: CustomState,
105
+ ): ElementNode | undefined {
106
+ const result = getChildrenByPosition(myApp, x, y).filter((el) =>
107
+ hasCustomState(el, customState),
108
+ );
109
+
110
+ if (result.length === 0) {
111
+ return undefined;
112
+ }
113
+
114
+ let element: ElementNode | undefined = result[result.length - 1];
115
+
116
+ while (element) {
117
+ const elmParent = element.parent;
118
+ if (elmParent?.forwardStates && hasCustomState(elmParent, customState)) {
119
+ element = elmParent;
120
+ } else {
121
+ break;
122
+ }
123
+ }
124
+
125
+ return element;
126
+ }
127
+
128
+ function findElementByActiveElement(e: MouseEvent): ElementNode | null {
63
129
  const active = activeElement();
64
130
  const precision = Config.rendererOptions?.deviceLogicalPixelRatio || 1;
131
+
65
132
  if (
66
133
  active instanceof ElementNode &&
67
134
  testCollision(
@@ -73,38 +140,95 @@ const handleClick = (e: MouseEvent): void => {
73
140
  (active.height || 0) * precision,
74
141
  )
75
142
  ) {
76
- if (isFunc(active.onMouseClick)) {
77
- active.onMouseClick.call(active, e, active);
78
- return;
143
+ return active;
144
+ }
145
+
146
+ let parent = active?.parent;
147
+ while (parent) {
148
+ if (
149
+ isFunc(parent.onMouseClick) &&
150
+ active &&
151
+ testCollision(
152
+ e.clientX,
153
+ e.clientY,
154
+ ((parent.lng.absX as number) || 0) * precision,
155
+ ((parent.lng.absY as number) || 0) * precision,
156
+ (parent.width || 0) * precision,
157
+ (parent.height || 0) * precision,
158
+ )
159
+ ) {
160
+ return parent;
79
161
  }
162
+ parent = parent.parent;
163
+ }
164
+
165
+ return null;
166
+ }
167
+
168
+ function applyPressedState(
169
+ element: ElementNode,
170
+ pressedState: CustomState,
171
+ pressedStateDuration: number = DEFAULT_PRESSED_STATE_DURATION,
172
+ ): void {
173
+ addCustomStateToElement(element, pressedState);
174
+ setTimeout(() => {
175
+ removeCustomStateFromElement(element, pressedState);
176
+ }, pressedStateDuration);
177
+ }
178
+
179
+ function handleElementClick(
180
+ clickedElement: ElementNode,
181
+ e: MouseEvent,
182
+ customStates?: MouseStateOptions,
183
+ ): void {
184
+ if (customStates?.pressedState) {
185
+ applyPressedState(
186
+ clickedElement,
187
+ customStates.pressedState,
188
+ customStates.pressedStateDuration,
189
+ );
190
+ }
191
+
192
+ if (isFunc(clickedElement.onMouseClick)) {
193
+ clickedElement.onMouseClick(e, clickedElement);
194
+ return;
195
+ } else if (isFunc(clickedElement.onEnter)) {
196
+ clickedElement.onEnter();
197
+ return;
198
+ }
80
199
 
200
+ clickedElement.setFocus();
201
+ setTimeout(() => {
81
202
  document.dispatchEvent(createKeyboardEvent('Enter', 13));
82
203
  setTimeout(
83
204
  () =>
84
205
  document.body.dispatchEvent(createKeyboardEvent('Enter', 13, 'keyup')),
85
206
  1,
86
207
  );
87
- } else {
88
- let parent = active?.parent;
89
- while (parent) {
90
- if (
91
- isFunc(parent.onMouseClick) &&
92
- testCollision(
208
+ }, 1);
209
+ }
210
+
211
+ function createHandleClick<TApp extends ElementNode>(
212
+ myApp: TApp,
213
+ customStates?: MouseStateOptions,
214
+ ) {
215
+ return (e: MouseEvent): void => {
216
+ const clickedElement = customStates
217
+ ? findElementWithCustomState(
218
+ myApp,
93
219
  e.clientX,
94
220
  e.clientY,
95
- ((parent.lng.absX as number) || 0) * precision,
96
- ((parent.lng.absY as number) || 0) * precision,
97
- (parent.width || 0) * precision,
98
- (parent.height || 0) * precision,
221
+ customStates.hoverState,
99
222
  )
100
- ) {
101
- parent.onMouseClick.call(parent, e, active!);
102
- return;
103
- }
104
- parent = parent.parent;
223
+ : findElementByActiveElement(e);
224
+
225
+ if (!clickedElement) {
226
+ return;
105
227
  }
106
- }
107
- };
228
+
229
+ handleElementClick(clickedElement, e, customStates);
230
+ };
231
+ }
108
232
 
109
233
  function testCollision(
110
234
  px: number,
@@ -117,106 +241,155 @@ function testCollision(
117
241
  return px >= cx && px <= cx + cw && py >= cy && py <= cy + ch;
118
242
  }
119
243
 
120
- function getChildrenByPosition(
121
- node: ElementNode,
244
+ function isNodeAtPosition(
245
+ node: ElementNode | ElementText | TextNode,
122
246
  x: number,
123
247
  y: number,
124
- ): ElementNode[] {
125
- const result: ElementNode[] = [];
126
- const precision = Config.rendererOptions?.deviceLogicalPixelRatio || 1;
248
+ precision: number,
249
+ ): node is ElementNode {
250
+ if (!isElementNode(node)) {
251
+ return false;
252
+ }
253
+
254
+ return (
255
+ node.alpha !== 0 &&
256
+ !node.skipFocus &&
257
+ testCollision(
258
+ x,
259
+ y,
260
+ ((node.lng.absX as number) || 0) * precision,
261
+ ((node.lng.absY as number) || 0) * precision,
262
+ (node.width || 0) * precision,
263
+ (node.height || 0) * precision,
264
+ )
265
+ );
266
+ }
267
+
268
+ function findHighestZIndexNode(nodes: ElementNode[]): ElementNode | undefined {
269
+ if (nodes.length === 0) {
270
+ return undefined;
271
+ }
272
+
273
+ if (nodes.length === 1) {
274
+ return nodes[0];
275
+ }
276
+
277
+ let maxZIndex = -1;
278
+ let highestNode: ElementNode | undefined = undefined;
279
+
280
+ for (const node of nodes) {
281
+ const zIndex = node.zIndex ?? -1;
282
+ if (zIndex >= maxZIndex) {
283
+ maxZIndex = zIndex;
284
+ highestNode = node;
285
+ }
286
+ }
127
287
 
288
+ return highestNode;
289
+ }
290
+
291
+ function getChildrenByPosition<TElement extends ElementNode = ElementNode>(
292
+ node: TElement,
293
+ x: number,
294
+ y: number,
295
+ ): TElement[] {
296
+ const result: TElement[] = [];
297
+ const precision = Config.rendererOptions?.deviceLogicalPixelRatio || 1;
128
298
  // Queue for BFS
299
+
129
300
  let queue: (ElementNode | ElementText | TextNode)[] = [node];
130
301
 
131
302
  while (queue.length > 0) {
132
303
  // Process nodes at the current level
133
- const currentLevelNodes: ElementNode[] = [];
134
-
135
- for (const currentNode of queue) {
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
- }
304
+ const currentLevelNodes = queue.filter((currentNode) =>
305
+ isNodeAtPosition(currentNode, x, y, precision),
306
+ );
152
307
 
153
- const size = currentLevelNodes.length;
154
- if (size === 0) {
308
+ if (currentLevelNodes.length === 0) {
155
309
  break;
156
310
  }
157
311
 
158
- let highestZIndexNode = null;
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
- }
312
+ const highestZIndexNode = findHighestZIndexNode(currentLevelNodes);
174
313
 
175
- if (highestZIndexNode && !isTextNode(highestZIndexNode)) {
176
- result.push(highestZIndexNode);
177
- queue = highestZIndexNode.children;
178
- } else {
179
- queue = [];
314
+ if (!highestZIndexNode || isTextNode(highestZIndexNode)) {
315
+ break;
180
316
  }
317
+
318
+ result.push(highestZIndexNode as TElement);
319
+ queue = highestZIndexNode.children;
181
320
  }
182
321
 
183
322
  return result;
184
323
  }
185
324
 
186
- export function useMouse(
187
- myApp: ElementNode = rootNode,
325
+ export function useMouse<TApp extends ElementNode = ElementNode>(
326
+ myApp: TApp = rootNode as TApp,
188
327
  throttleBy: number = 100,
328
+ options?: UseMouseOptions,
189
329
  ): void {
190
330
  const pos = useMousePosition();
191
331
  const scheduled = createScheduled((fn) => throttle(fn, throttleBy));
332
+ let previousElement: ElementNode | null = null;
333
+ const customStates = options?.customStates;
334
+ const hoverState = customStates?.hoverState;
335
+ const handleClick = createHandleClick(myApp, customStates);
336
+ const owner = getOwner();
337
+ const handleClickContext = (e: MouseEvent) => {
338
+ runWithOwner(owner, () => handleClick(e));
339
+ };
340
+
192
341
  makeEventListener(window, 'wheel', handleScroll);
193
- makeEventListener(window, 'click', handleClick);
342
+ makeEventListener(window, 'click', handleClickContext);
194
343
  createEffect(() => {
195
344
  if (scheduled()) {
196
345
  const result = getChildrenByPosition(myApp, pos.x, pos.y).filter(
197
- (el) => el.focus || el.onFocus || el.onEnter,
346
+ (el) =>
347
+ !!(
348
+ el.onEnter ||
349
+ el.onMouseClick ||
350
+ el.onFocus ||
351
+ el[Config.focusStateKey] ||
352
+ (hoverState ? el[hoverState] : false)
353
+ ),
198
354
  );
199
355
 
200
356
  if (result.length) {
201
- let activeElm = result[result.length - 1];
357
+ let activeElm: ElementNode | undefined = result[result.length - 1];
202
358
 
203
359
  while (activeElm) {
204
360
  const elmParent = activeElm.parent;
205
361
  if (elmParent?.forwardStates) {
206
- activeElm = activeElm.parent;
362
+ activeElm = elmParent;
207
363
  } else {
208
364
  break;
209
365
  }
210
366
  }
211
367
 
368
+ if (!activeElm) {
369
+ return;
370
+ }
371
+
212
372
  // Update Row & Column Selected property
213
- const activeElmParent = activeElm?.parent;
214
- if (activeElm && activeElmParent?.selected !== undefined) {
373
+ const activeElmParent = activeElm.parent;
374
+ if (activeElmParent?.selected !== undefined) {
215
375
  activeElmParent.selected =
216
376
  activeElmParent.children.indexOf(activeElm);
217
377
  }
218
378
 
219
- activeElm?.setFocus();
379
+ if (previousElement && previousElement !== activeElm && hoverState) {
380
+ removeCustomStateFromElement(previousElement, hoverState);
381
+ }
382
+
383
+ if (hoverState) {
384
+ addCustomStateToElement(activeElm, hoverState);
385
+ } else {
386
+ activeElm.setFocus();
387
+ }
388
+
389
+ previousElement = activeElm;
390
+ } else if (previousElement && hoverState) {
391
+ removeCustomStateFromElement(previousElement, hoverState);
392
+ previousElement = null;
220
393
  }
221
394
  }
222
395
  });