@maestro_io/maestro-web-sdk 2.1.0 → 2.1.2
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/MaestroEventDelegate.js +60 -0
- package/dist/components/atoms/BaseButton/BaseButton.js +11 -0
- package/dist/components/atoms/BaseButton/index.js +2 -0
- package/dist/components/atoms/Image/index.js +30 -0
- package/dist/components/atoms/Rive/index.d.ts +7 -8
- package/dist/components/atoms/Rive/index.js +52 -0
- package/dist/components/atoms/SvgIcon/BetGeneralIcon.js +9 -0
- package/dist/components/atoms/SvgIcon/BetsIcon.js +11 -0
- package/dist/components/atoms/SvgIcon/BetsWarningIcon.js +11 -0
- package/dist/components/atoms/SvgIcon/CheckmarkIcon.js +6 -0
- package/dist/components/atoms/SvgIcon/CollapseIcon.js +6 -0
- package/dist/components/atoms/SvgIcon/ExpandIcon.js +6 -0
- package/dist/components/atoms/SvgIcon/FantasyIcon.js +8 -0
- package/dist/components/atoms/SvgIcon/FlameIcon.js +11 -0
- package/dist/components/atoms/SvgIcon/Icon.js +42 -0
- package/dist/components/atoms/SvgIcon/KeyPlaysIcon.js +10 -0
- package/dist/components/atoms/SvgIcon/MobilePhoneIcon.js +11 -0
- package/dist/components/atoms/SvgIcon/PlayIcon.js +8 -0
- package/dist/components/atoms/SvgIcon/StatsIcon.js +12 -0
- package/dist/components/atoms/SvgIcon/TimesIcon.js +6 -0
- package/dist/components/atoms/SvgIcon/WarningIcon.js +8 -0
- package/dist/components/atoms/SvgIcon/index.js +2 -0
- package/dist/components/core/App/App.js +41 -0
- package/dist/components/core/App/index.js +2 -0
- package/dist/components/core/OverlayContainer.js +58 -0
- package/dist/components/core/PanelManager/PanelManager.js +80 -0
- package/dist/components/core/PanelManager/index.js +2 -0
- package/dist/components/core/ScrollableContainer/FocusableItem.js +38 -0
- package/dist/components/core/ScrollableContainer/ScrollableContainer.js +166 -0
- package/dist/components/core/ScrollableContainer/index.js +2 -0
- package/dist/components/molecules/ActionButton/ActionButton.js +10 -0
- package/dist/components/molecules/ActionButton/index.js +2 -0
- package/dist/components/molecules/Overlay/Overlay.d.ts +1 -2
- package/dist/components/molecules/Overlay/Overlay.js +139 -0
- package/dist/components/molecules/Overlay/index.js +2 -0
- package/dist/components/molecules/PanelNavButton/PanelNavButton.js +40 -0
- package/dist/components/molecules/PanelNavButton/index.js +2 -0
- package/dist/components/molecules/SegmentButton/SegmentButton.js +10 -0
- package/dist/components/molecules/SegmentButton/index.js +2 -0
- package/dist/components/molecules/WatchButton/WatchButton.js +15 -0
- package/dist/components/molecules/WatchButton/index.js +2 -0
- package/dist/components/organisms/PanelNavigation/PanelNavigation.js +72 -0
- package/dist/components/organisms/PanelNavigation/index.js +2 -0
- package/dist/components/organisms/SegmentController/SegmentController.js +69 -0
- package/dist/components/organisms/SegmentController/index.d.ts +1 -1
- package/dist/components/organisms/SegmentController/index.js +2 -0
- package/dist/external/AxiosNetworkClient.js +83 -0
- package/dist/external/LocalStorageCacheManager.js +213 -0
- package/dist/external/ReactRenderer.d.ts +2 -2
- package/dist/external/ReactRenderer.js +86 -0
- package/dist/external/mocks/MockExternalNetworkClient.js +161 -0
- package/dist/external/ports/cacheManager.js +1 -0
- package/dist/external/ports/networkClient.js +1 -0
- package/dist/external/ports/renderer.js +1 -0
- package/dist/external/spatial-navigation/index.d.ts +1 -1
- package/dist/external/spatial-navigation/index.js +4 -0
- package/dist/external/spatial-navigation/measureLayout.js +30 -0
- package/dist/external/spatial-navigation/spatialNavigation.js +875 -0
- package/dist/external/spatial-navigation/utils.js +6 -0
- package/dist/external/spatial-navigation/visualDebugger.js +76 -0
- package/dist/external/spatial-navigation/withFocusable.js +238 -0
- package/dist/helpers/Observable.js +37 -0
- package/dist/helpers/deliver-focus.js +12 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +192 -0
- package/dist/interfaces/IMaestroEvent.js +6 -0
- package/dist/interfaces/IMaestroEventDelegate.js +1 -0
- package/dist/interfaces/IMaestroManager.js +1 -0
- package/dist/maestro-web-sdk.umd.js +3 -3
- package/dist/maestro-web-sdk.umd.js.map +1 -1
- package/dist/models/IModel.js +1 -0
- package/dist/models/IPanel.js +53 -0
- package/dist/models/ITheme.js +20 -0
- package/dist/models/IWinningBet.js +1 -0
- package/dist/modules/bets/interfaces/IBets.js +1 -0
- package/dist/modules/bets/mocks.js +572 -0
- package/dist/modules/bets/view/BetsView.js +116 -0
- package/dist/modules/bets/view/HotProps/HotPropsView.js +139 -0
- package/dist/modules/bets/view/SixPack/SixPackView.js +103 -0
- package/dist/modules/bets/view/TabLoadFailureView.js +12 -0
- package/dist/modules/bets/view/UserBets/BetResult/BetResult.js +6 -0
- package/dist/modules/bets/view/UserBets/BetsCard/BetsCard.js +122 -0
- package/dist/modules/bets/view/UserBets/BetsCard/BetsCardEvent.js +34 -0
- package/dist/modules/bets/view/UserBets/BetsCard/BetsCardFooter.js +22 -0
- package/dist/modules/bets/view/UserBets/Boost/Boost.js +6 -0
- package/dist/modules/bets/view/UserBets/OutcomeBadge/OutcomeBadge.js +9 -0
- package/dist/modules/bets/view/UserBets/Parlay/Parlay.js +13 -0
- package/dist/modules/bets/view/UserBets/Straight/Straight.js +6 -0
- package/dist/modules/bets/view/UserBets/Teaser/Teaser.js +13 -0
- package/dist/modules/bets/view/UserBets/UserBetsView.js +114 -0
- package/dist/modules/bets/view/components/Bet/Bet.js +32 -0
- package/dist/modules/bets/view/components/BetPill/BetPill.js +6 -0
- package/dist/modules/bets/view/components/BetsCard/BetsCard.js +15 -0
- package/dist/modules/bets/view/components/BetsCard/BetsCardHeader.js +20 -0
- package/dist/modules/bets/view/components/GamblingDisclaimer/GamblingDisclaimer.js +14 -0
- package/dist/modules/bets/view/components/LocationWarning/LocationWarning.js +16 -0
- package/dist/modules/bets/view/components/PromoCodeBanner/PromoCodeBanner.js +63 -0
- package/dist/modules/bets/view/components/PromoCodeBanner/index.d.ts +1 -1
- package/dist/modules/bets/view/components/PromoCodeBanner/index.js +1 -0
- package/dist/modules/bets/view-model/BetsViewModel.js +767 -0
- package/dist/modules/key-plays/constants.js +292 -0
- package/dist/modules/key-plays/interfaces/IKeyPlays.d.ts +4 -4
- package/dist/modules/key-plays/interfaces/IKeyPlays.js +14 -0
- package/dist/modules/key-plays/interfaces/IKeyPlaysResponse.js +1 -0
- package/dist/modules/key-plays/view/KeyPlayCardScoreTagView.js +13 -0
- package/dist/modules/key-plays/view/KeyPlayCardView.js +142 -0
- package/dist/modules/key-plays/view/KeyPlayErrorStateView.js +58 -0
- package/dist/modules/key-plays/view/KeyPlaysView.js +167 -0
- package/dist/modules/key-plays/view-model/KeyPlaysViewModel.js +182 -0
- package/dist/modules/stats/interfaces/IStats.d.ts +163 -0
- package/dist/modules/stats/interfaces/IStats.js +1 -0
- package/dist/modules/stats/mocks.d.ts +2 -0
- package/dist/modules/stats/mocks.js +546 -0
- package/dist/modules/stats/utils.d.ts +39 -0
- package/dist/modules/stats/utils.js +62 -0
- package/dist/modules/stats/view/GameLeaders/GameLeaders.d.ts +29 -0
- package/dist/modules/stats/view/GameLeaders/GameLeaders.js +9 -0
- package/dist/modules/stats/view/GameLeaders/index.d.ts +2 -0
- package/dist/modules/stats/view/GameLeaders/index.js +2 -0
- package/dist/modules/stats/view/MatchPredictor/DonutChart/DonutChart.d.ts +17 -0
- package/dist/modules/stats/view/MatchPredictor/DonutChart/DonutChart.js +55 -0
- package/dist/modules/stats/view/MatchPredictor/DonutChart/index.d.ts +2 -0
- package/dist/modules/stats/view/MatchPredictor/DonutChart/index.js +1 -0
- package/dist/modules/stats/view/MatchPredictor/MatchPredictorView.d.ts +52 -0
- package/dist/modules/stats/view/MatchPredictor/MatchPredictorView.js +66 -0
- package/dist/modules/stats/view/MatchPredictor/index.d.ts +3 -0
- package/dist/modules/stats/view/MatchPredictor/index.js +2 -0
- package/dist/modules/stats/view/RecentGames/RecentGames.d.ts +62 -0
- package/dist/modules/stats/view/RecentGames/RecentGames.js +22 -0
- package/dist/modules/stats/view/RecentGames/index.d.ts +2 -0
- package/dist/modules/stats/view/RecentGames/index.js +2 -0
- package/dist/modules/stats/view/StatsQRCode/StatsQRCodeView.d.ts +51 -0
- package/dist/modules/stats/view/StatsQRCode/StatsQRCodeView.js +38 -0
- package/dist/modules/stats/view/StatsQRCode/index.d.ts +2 -0
- package/dist/modules/stats/view/StatsQRCode/index.js +1 -0
- package/dist/modules/stats/view/StatsView.d.ts +20 -0
- package/dist/modules/stats/view/StatsView.js +87 -0
- package/dist/modules/stats/view/Teams/StatsCarousel.d.ts +8 -0
- package/dist/modules/stats/view/Teams/StatsCarousel.js +14 -0
- package/dist/modules/stats/view/Teams/StatsProgressBar.d.ts +7 -0
- package/dist/modules/stats/view/Teams/StatsProgressBar.js +14 -0
- package/dist/modules/stats/view/Teams/Teams.d.ts +67 -0
- package/dist/modules/stats/view/Teams/Teams.js +35 -0
- package/dist/modules/stats/view/Teams/index.d.ts +2 -0
- package/dist/modules/stats/view/Teams/index.js +2 -0
- package/dist/modules/stats/view/WinProbability/WinProbability.d.ts +62 -0
- package/dist/modules/stats/view/WinProbability/WinProbability.js +27 -0
- package/dist/modules/stats/view/WinProbability/index.d.ts +2 -0
- package/dist/modules/stats/view/WinProbability/index.js +2 -0
- package/dist/modules/stats/view/components/AthletesStatistics/AthletesStatistics.d.ts +51 -0
- package/dist/modules/stats/view/components/AthletesStatistics/AthletesStatistics.js +28 -0
- package/dist/modules/stats/view/components/AthletesStatistics/StatisticTable.d.ts +10 -0
- package/dist/modules/stats/view/components/AthletesStatistics/StatisticTable.js +11 -0
- package/dist/modules/stats/view/components/AthletesStatistics/index.d.ts +2 -0
- package/dist/modules/stats/view/components/AthletesStatistics/index.js +2 -0
- package/dist/modules/stats/view/index.d.ts +1 -0
- package/dist/modules/stats/view/index.js +1 -0
- package/dist/modules/stats/view-model/StatsViewModel.d.ts +31 -0
- package/dist/modules/stats/view-model/StatsViewModel.js +90 -0
- package/dist/services/BetsService.js +102 -0
- package/dist/services/NetworkManager/NetworkManager.js +410 -0
- package/dist/services/NetworkManager/errors.js +45 -0
- package/dist/services/ThemeManager/constants.js +47 -0
- package/dist/test-polyfills.d.ts +1 -0
- package/dist/test-polyfills.js +15 -0
- package/dist/types/OverlayTypes.js +4 -0
- package/dist/view-models/MaestroEventViewModel.d.ts +2 -0
- package/dist/view-models/MaestroEventViewModel.js +156 -0
- package/dist/view-models/OverlayViewModel.js +222 -0
- package/dist/view-models/PanelManagerViewModel.js +80 -0
- package/dist/view-models/ViewModel.js +32 -0
- package/dist/view-models/index.js +59 -0
- package/package.json +1 -1
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
import measureLayout from './measureLayout';
|
|
2
|
+
import { KEY_BACK } from './utils';
|
|
3
|
+
import { DIRECTION_DOWN, KEY_ENTER } from './utils';
|
|
4
|
+
import { DIRECTION_LEFT, DIRECTION_RIGHT, DIRECTION_UP } from './utils';
|
|
5
|
+
import VisualDebugger from './visualDebugger';
|
|
6
|
+
export const ROOT_FOCUS_KEY = 'SN:ROOT';
|
|
7
|
+
const ADJACENT_SLICE_THRESHOLD = 0.2;
|
|
8
|
+
/**
|
|
9
|
+
* Adjacent slice is 5 times more important than diagonal
|
|
10
|
+
*/
|
|
11
|
+
const ADJACENT_SLICE_WEIGHT = 5;
|
|
12
|
+
const DIAGONAL_SLICE_WEIGHT = 1;
|
|
13
|
+
/**
|
|
14
|
+
* Main coordinate distance is 5 times more important
|
|
15
|
+
*/
|
|
16
|
+
const MAIN_COORDINATE_WEIGHT = 5;
|
|
17
|
+
const DEFAULT_KEY_MAP = {
|
|
18
|
+
[DIRECTION_LEFT]: [37],
|
|
19
|
+
[DIRECTION_UP]: [38],
|
|
20
|
+
[DIRECTION_RIGHT]: [39],
|
|
21
|
+
[DIRECTION_DOWN]: [40],
|
|
22
|
+
[KEY_ENTER]: [13],
|
|
23
|
+
[KEY_BACK]: [8, 10009],
|
|
24
|
+
};
|
|
25
|
+
const DEBUG_FN_COLORS = ['#0FF', '#FF0', '#F0F'];
|
|
26
|
+
const THROTTLE_OPTIONS = {
|
|
27
|
+
leading: true,
|
|
28
|
+
trailing: false,
|
|
29
|
+
};
|
|
30
|
+
export const getChildClosestToOrigin = (children) => {
|
|
31
|
+
const childrenClosestToOrigin = sortBy(children, ({ layout }) => Math.abs(layout.left) + Math.abs(layout.top));
|
|
32
|
+
return first(childrenClosestToOrigin);
|
|
33
|
+
};
|
|
34
|
+
const throttle = (func, wait, options = {}) => {
|
|
35
|
+
let timeout = null;
|
|
36
|
+
let previous = 0;
|
|
37
|
+
const { leading = true, trailing = true } = options;
|
|
38
|
+
return function (...args) {
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
if (!previous && !leading)
|
|
41
|
+
previous = now;
|
|
42
|
+
const remaining = wait - (now - previous);
|
|
43
|
+
if (remaining <= 0 || remaining > wait) {
|
|
44
|
+
if (timeout) {
|
|
45
|
+
clearTimeout(timeout);
|
|
46
|
+
timeout = null;
|
|
47
|
+
}
|
|
48
|
+
previous = now;
|
|
49
|
+
func.apply(this, args);
|
|
50
|
+
}
|
|
51
|
+
else if (!timeout && trailing) {
|
|
52
|
+
timeout = window.setTimeout(() => {
|
|
53
|
+
previous = leading ? Date.now() : 0;
|
|
54
|
+
timeout = null;
|
|
55
|
+
func.apply(this, args);
|
|
56
|
+
}, remaining);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
const sortBy = (array, iteratee) => [...array].sort((a, b) => {
|
|
61
|
+
const aValue = iteratee(a);
|
|
62
|
+
const bValue = iteratee(b);
|
|
63
|
+
return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
|
|
64
|
+
});
|
|
65
|
+
const first = (array) => array[0];
|
|
66
|
+
const difference = (array, values) => array.filter((x) => !values.includes(x));
|
|
67
|
+
const forEach = (array, iteratee) => array.forEach(iteratee);
|
|
68
|
+
/* eslint-disable no-nested-ternary */
|
|
69
|
+
class SpatialNavigation {
|
|
70
|
+
/**
|
|
71
|
+
* Used to determine the coordinate that will be used to filter items that are over the "edge"
|
|
72
|
+
*/
|
|
73
|
+
static getCutoffCoordinate(isVertical, isIncremental, isSibling, layout) {
|
|
74
|
+
const itemX = layout.left;
|
|
75
|
+
const itemY = layout.top;
|
|
76
|
+
const itemWidth = layout.width;
|
|
77
|
+
const itemHeight = layout.height;
|
|
78
|
+
const coordinate = isVertical ? itemY : itemX;
|
|
79
|
+
const itemSize = isVertical ? itemHeight : itemWidth;
|
|
80
|
+
return isIncremental
|
|
81
|
+
? isSibling
|
|
82
|
+
? coordinate
|
|
83
|
+
: coordinate + itemSize
|
|
84
|
+
: isSibling
|
|
85
|
+
? coordinate + itemSize
|
|
86
|
+
: coordinate;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Returns two corners (a and b) coordinates that are used as a reference points
|
|
90
|
+
* Where "a" is always leftmost and topmost corner, and "b" is rightmost bottommost corner
|
|
91
|
+
*/
|
|
92
|
+
static getRefCorners(direction, isSibling, layout) {
|
|
93
|
+
const itemX = layout.left;
|
|
94
|
+
const itemY = layout.top;
|
|
95
|
+
const itemWidth = layout.width;
|
|
96
|
+
const itemHeight = layout.height;
|
|
97
|
+
const result = {
|
|
98
|
+
a: {
|
|
99
|
+
x: 0,
|
|
100
|
+
y: 0,
|
|
101
|
+
},
|
|
102
|
+
b: {
|
|
103
|
+
x: 0,
|
|
104
|
+
y: 0,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
switch (direction) {
|
|
108
|
+
case DIRECTION_UP: {
|
|
109
|
+
const y = isSibling ? itemY + itemHeight : itemY;
|
|
110
|
+
result.a = {
|
|
111
|
+
x: itemX,
|
|
112
|
+
y,
|
|
113
|
+
};
|
|
114
|
+
result.b = {
|
|
115
|
+
x: itemX + itemWidth,
|
|
116
|
+
y,
|
|
117
|
+
};
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case DIRECTION_DOWN: {
|
|
121
|
+
const y = isSibling ? itemY : itemY + itemHeight;
|
|
122
|
+
result.a = {
|
|
123
|
+
x: itemX,
|
|
124
|
+
y,
|
|
125
|
+
};
|
|
126
|
+
result.b = {
|
|
127
|
+
x: itemX + itemWidth,
|
|
128
|
+
y,
|
|
129
|
+
};
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case DIRECTION_LEFT: {
|
|
133
|
+
const x = isSibling ? itemX + itemWidth : itemX;
|
|
134
|
+
result.a = {
|
|
135
|
+
x,
|
|
136
|
+
y: itemY,
|
|
137
|
+
};
|
|
138
|
+
result.b = {
|
|
139
|
+
x,
|
|
140
|
+
y: itemY + itemHeight,
|
|
141
|
+
};
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case DIRECTION_RIGHT: {
|
|
145
|
+
const x = isSibling ? itemX : itemX + itemWidth;
|
|
146
|
+
result.a = {
|
|
147
|
+
x,
|
|
148
|
+
y: itemY,
|
|
149
|
+
};
|
|
150
|
+
result.b = {
|
|
151
|
+
x,
|
|
152
|
+
y: itemY + itemHeight,
|
|
153
|
+
};
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
default:
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Calculates if the sibling node is intersecting enough with the ref node by the secondary coordinate
|
|
163
|
+
*/
|
|
164
|
+
static isAdjacentSlice(refCorners, siblingCorners, isVerticalDirection) {
|
|
165
|
+
const { a: refA, b: refB } = refCorners;
|
|
166
|
+
const { a: siblingA, b: siblingB } = siblingCorners;
|
|
167
|
+
const coordinate = isVerticalDirection ? 'x' : 'y';
|
|
168
|
+
const refCoordinateA = refA[coordinate];
|
|
169
|
+
const refCoordinateB = refB[coordinate];
|
|
170
|
+
const siblingCoordinateA = siblingA[coordinate];
|
|
171
|
+
const siblingCoordinateB = siblingB[coordinate];
|
|
172
|
+
const thresholdDistance = (refCoordinateB - refCoordinateA) * ADJACENT_SLICE_THRESHOLD;
|
|
173
|
+
const intersectionLength = Math.max(0, Math.min(refCoordinateB, siblingCoordinateB) -
|
|
174
|
+
Math.max(refCoordinateA, siblingCoordinateA));
|
|
175
|
+
return intersectionLength >= thresholdDistance;
|
|
176
|
+
}
|
|
177
|
+
static getPrimaryAxisDistance(refCorners, siblingCorners, isVerticalDirection) {
|
|
178
|
+
const { a: refA } = refCorners;
|
|
179
|
+
const { a: siblingA } = siblingCorners;
|
|
180
|
+
const coordinate = isVerticalDirection ? 'y' : 'x';
|
|
181
|
+
return Math.abs(siblingA[coordinate] - refA[coordinate]);
|
|
182
|
+
}
|
|
183
|
+
static getSecondaryAxisDistance(refCorners, siblingCorners, isVerticalDirection) {
|
|
184
|
+
const { a: refA, b: refB } = refCorners;
|
|
185
|
+
const { a: siblingA, b: siblingB } = siblingCorners;
|
|
186
|
+
const coordinate = isVerticalDirection ? 'x' : 'y';
|
|
187
|
+
const refCoordinateA = refA[coordinate];
|
|
188
|
+
const refCoordinateB = refB[coordinate];
|
|
189
|
+
const siblingCoordinateA = siblingA[coordinate];
|
|
190
|
+
const siblingCoordinateB = siblingB[coordinate];
|
|
191
|
+
const distancesToCompare = [];
|
|
192
|
+
distancesToCompare.push(Math.abs(siblingCoordinateA - refCoordinateA));
|
|
193
|
+
distancesToCompare.push(Math.abs(siblingCoordinateA - refCoordinateB));
|
|
194
|
+
distancesToCompare.push(Math.abs(siblingCoordinateB - refCoordinateA));
|
|
195
|
+
distancesToCompare.push(Math.abs(siblingCoordinateB - refCoordinateB));
|
|
196
|
+
return Math.min(...distancesToCompare);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Inspired by: https://developer.mozilla.org/en-US/docs/Mozilla/Firefox_OS_for_TV/TV_remote_control_navigation#Algorithm_design
|
|
200
|
+
* Ref Corners are the 2 corners of the current component in the direction of navigation
|
|
201
|
+
* They used as a base to measure adjacent slices
|
|
202
|
+
*/
|
|
203
|
+
sortSiblingsByPriority(siblings, currentLayout, direction, _focusKey) {
|
|
204
|
+
const isVerticalDirection = direction === DIRECTION_DOWN || direction === DIRECTION_UP;
|
|
205
|
+
const refCorners = SpatialNavigation.getRefCorners(direction, false, currentLayout);
|
|
206
|
+
return [...siblings].sort((a, b) => {
|
|
207
|
+
const aCorners = SpatialNavigation.getRefCorners(direction, true, a.layout);
|
|
208
|
+
const bCorners = SpatialNavigation.getRefCorners(direction, true, b.layout);
|
|
209
|
+
const aIsAdjacentSlice = SpatialNavigation.isAdjacentSlice(refCorners, aCorners, isVerticalDirection);
|
|
210
|
+
const bIsAdjacentSlice = SpatialNavigation.isAdjacentSlice(refCorners, bCorners, isVerticalDirection);
|
|
211
|
+
const aPrimaryAxisFunction = aIsAdjacentSlice
|
|
212
|
+
? SpatialNavigation.getPrimaryAxisDistance
|
|
213
|
+
: SpatialNavigation.getSecondaryAxisDistance;
|
|
214
|
+
const bPrimaryAxisFunction = bIsAdjacentSlice
|
|
215
|
+
? SpatialNavigation.getPrimaryAxisDistance
|
|
216
|
+
: SpatialNavigation.getSecondaryAxisDistance;
|
|
217
|
+
const aSecondaryAxisFunction = aIsAdjacentSlice
|
|
218
|
+
? SpatialNavigation.getSecondaryAxisDistance
|
|
219
|
+
: SpatialNavigation.getPrimaryAxisDistance;
|
|
220
|
+
const bSecondaryAxisFunction = bIsAdjacentSlice
|
|
221
|
+
? SpatialNavigation.getSecondaryAxisDistance
|
|
222
|
+
: SpatialNavigation.getPrimaryAxisDistance;
|
|
223
|
+
const aPrimaryAxisDistance = aPrimaryAxisFunction(refCorners, aCorners, isVerticalDirection);
|
|
224
|
+
const bPrimaryAxisDistance = bPrimaryAxisFunction(refCorners, bCorners, isVerticalDirection);
|
|
225
|
+
const aSecondaryAxisDistance = aSecondaryAxisFunction(refCorners, aCorners, isVerticalDirection);
|
|
226
|
+
const bSecondaryAxisDistance = bSecondaryAxisFunction(refCorners, bCorners, isVerticalDirection);
|
|
227
|
+
const aTotalDistancePoints = aPrimaryAxisDistance * MAIN_COORDINATE_WEIGHT +
|
|
228
|
+
aSecondaryAxisDistance;
|
|
229
|
+
const bTotalDistancePoints = bPrimaryAxisDistance * MAIN_COORDINATE_WEIGHT +
|
|
230
|
+
bSecondaryAxisDistance;
|
|
231
|
+
const aPriority = (aTotalDistancePoints + 1) /
|
|
232
|
+
(aIsAdjacentSlice ? ADJACENT_SLICE_WEIGHT : DIAGONAL_SLICE_WEIGHT);
|
|
233
|
+
const bPriority = (bTotalDistancePoints + 1) /
|
|
234
|
+
(bIsAdjacentSlice ? ADJACENT_SLICE_WEIGHT : DIAGONAL_SLICE_WEIGHT);
|
|
235
|
+
return aPriority - bPriority;
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
constructor() {
|
|
239
|
+
Object.defineProperty(this, "focusableComponents", {
|
|
240
|
+
enumerable: true,
|
|
241
|
+
configurable: true,
|
|
242
|
+
writable: true,
|
|
243
|
+
value: void 0
|
|
244
|
+
});
|
|
245
|
+
Object.defineProperty(this, "focusKey", {
|
|
246
|
+
enumerable: true,
|
|
247
|
+
configurable: true,
|
|
248
|
+
writable: true,
|
|
249
|
+
value: void 0
|
|
250
|
+
});
|
|
251
|
+
Object.defineProperty(this, "parentsHavingFocusedChild", {
|
|
252
|
+
enumerable: true,
|
|
253
|
+
configurable: true,
|
|
254
|
+
writable: true,
|
|
255
|
+
value: void 0
|
|
256
|
+
});
|
|
257
|
+
Object.defineProperty(this, "enabled", {
|
|
258
|
+
enumerable: true,
|
|
259
|
+
configurable: true,
|
|
260
|
+
writable: true,
|
|
261
|
+
value: void 0
|
|
262
|
+
});
|
|
263
|
+
Object.defineProperty(this, "nativeMode", {
|
|
264
|
+
enumerable: true,
|
|
265
|
+
configurable: true,
|
|
266
|
+
writable: true,
|
|
267
|
+
value: void 0
|
|
268
|
+
});
|
|
269
|
+
Object.defineProperty(this, "throttle", {
|
|
270
|
+
enumerable: true,
|
|
271
|
+
configurable: true,
|
|
272
|
+
writable: true,
|
|
273
|
+
value: void 0
|
|
274
|
+
});
|
|
275
|
+
Object.defineProperty(this, "throttleKeypresses", {
|
|
276
|
+
enumerable: true,
|
|
277
|
+
configurable: true,
|
|
278
|
+
writable: true,
|
|
279
|
+
value: void 0
|
|
280
|
+
});
|
|
281
|
+
Object.defineProperty(this, "pressedKeys", {
|
|
282
|
+
enumerable: true,
|
|
283
|
+
configurable: true,
|
|
284
|
+
writable: true,
|
|
285
|
+
value: void 0
|
|
286
|
+
});
|
|
287
|
+
Object.defineProperty(this, "paused", {
|
|
288
|
+
enumerable: true,
|
|
289
|
+
configurable: true,
|
|
290
|
+
writable: true,
|
|
291
|
+
value: void 0
|
|
292
|
+
});
|
|
293
|
+
Object.defineProperty(this, "keyDownEventListener", {
|
|
294
|
+
enumerable: true,
|
|
295
|
+
configurable: true,
|
|
296
|
+
writable: true,
|
|
297
|
+
value: void 0
|
|
298
|
+
});
|
|
299
|
+
Object.defineProperty(this, "keyUpEventListener", {
|
|
300
|
+
enumerable: true,
|
|
301
|
+
configurable: true,
|
|
302
|
+
writable: true,
|
|
303
|
+
value: void 0
|
|
304
|
+
});
|
|
305
|
+
Object.defineProperty(this, "keyMap", {
|
|
306
|
+
enumerable: true,
|
|
307
|
+
configurable: true,
|
|
308
|
+
writable: true,
|
|
309
|
+
value: void 0
|
|
310
|
+
});
|
|
311
|
+
Object.defineProperty(this, "debug", {
|
|
312
|
+
enumerable: true,
|
|
313
|
+
configurable: true,
|
|
314
|
+
writable: true,
|
|
315
|
+
value: void 0
|
|
316
|
+
});
|
|
317
|
+
Object.defineProperty(this, "visualDebugger", {
|
|
318
|
+
enumerable: true,
|
|
319
|
+
configurable: true,
|
|
320
|
+
writable: true,
|
|
321
|
+
value: void 0
|
|
322
|
+
});
|
|
323
|
+
Object.defineProperty(this, "logIndex", {
|
|
324
|
+
enumerable: true,
|
|
325
|
+
configurable: true,
|
|
326
|
+
writable: true,
|
|
327
|
+
value: void 0
|
|
328
|
+
});
|
|
329
|
+
this.focusableComponents = {};
|
|
330
|
+
this.focusKey = null;
|
|
331
|
+
this.parentsHavingFocusedChild = [];
|
|
332
|
+
this.enabled = false;
|
|
333
|
+
this.nativeMode = false;
|
|
334
|
+
this.throttle = 0;
|
|
335
|
+
this.throttleKeypresses = false;
|
|
336
|
+
this.pressedKeys = {};
|
|
337
|
+
this.paused = false;
|
|
338
|
+
this.keyDownEventListener = null;
|
|
339
|
+
this.keyUpEventListener = null;
|
|
340
|
+
this.keyMap = DEFAULT_KEY_MAP;
|
|
341
|
+
this.debug = false;
|
|
342
|
+
this.visualDebugger = null;
|
|
343
|
+
this.logIndex = 0;
|
|
344
|
+
this.onKeyEvent = this.onKeyEvent.bind(this);
|
|
345
|
+
this.pause = this.pause.bind(this);
|
|
346
|
+
this.resume = this.resume.bind(this);
|
|
347
|
+
this.setFocus = this.setFocus.bind(this);
|
|
348
|
+
this.updateAllLayouts = this.updateAllLayouts.bind(this);
|
|
349
|
+
this.navigateByDirection = this.navigateByDirection.bind(this);
|
|
350
|
+
this.init = this.init.bind(this);
|
|
351
|
+
this.setKeyMap = this.setKeyMap.bind(this);
|
|
352
|
+
}
|
|
353
|
+
init({ debug = false, visualDebug = false, nativeMode = false, throttle = 0, throttleKeypresses = false, } = {}) {
|
|
354
|
+
if (!this.enabled) {
|
|
355
|
+
this.enabled = true;
|
|
356
|
+
this.nativeMode = nativeMode;
|
|
357
|
+
this.throttleKeypresses = throttleKeypresses;
|
|
358
|
+
this.debug = debug;
|
|
359
|
+
if (!this.nativeMode) {
|
|
360
|
+
if (Number.isInteger(throttle) && throttle > 0) {
|
|
361
|
+
this.throttle = throttle;
|
|
362
|
+
}
|
|
363
|
+
this.bindEventHandlers();
|
|
364
|
+
if (visualDebug) {
|
|
365
|
+
this.visualDebugger = new VisualDebugger();
|
|
366
|
+
this.startDrawLayouts();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
startDrawLayouts() {
|
|
372
|
+
const draw = () => {
|
|
373
|
+
requestAnimationFrame(() => {
|
|
374
|
+
this.visualDebugger?.clearLayouts();
|
|
375
|
+
Object.entries(this.focusableComponents).forEach(([focusKey, component]) => {
|
|
376
|
+
this.visualDebugger?.drawLayout(component.layout, focusKey, component.parentFocusKey);
|
|
377
|
+
});
|
|
378
|
+
draw();
|
|
379
|
+
});
|
|
380
|
+
};
|
|
381
|
+
draw();
|
|
382
|
+
}
|
|
383
|
+
destroy() {
|
|
384
|
+
if (this.enabled) {
|
|
385
|
+
this.enabled = false;
|
|
386
|
+
this.nativeMode = false;
|
|
387
|
+
this.throttle = 0;
|
|
388
|
+
this.throttleKeypresses = false;
|
|
389
|
+
this.focusKey = null;
|
|
390
|
+
this.parentsHavingFocusedChild = [];
|
|
391
|
+
this.focusableComponents = {};
|
|
392
|
+
this.paused = false;
|
|
393
|
+
this.keyMap = DEFAULT_KEY_MAP;
|
|
394
|
+
this.unbindEventHandlers();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
getEventType(keyCode) {
|
|
398
|
+
return Object.keys(this.getKeyMap()).find((key) => this.getKeyMap()[key].includes(keyCode));
|
|
399
|
+
}
|
|
400
|
+
bindEventHandlers() {
|
|
401
|
+
if (typeof window !== 'undefined' && window.addEventListener) {
|
|
402
|
+
const keyDownHandler = (event) => {
|
|
403
|
+
if (this.paused === true) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (this.debug) {
|
|
407
|
+
this.logIndex += 1;
|
|
408
|
+
}
|
|
409
|
+
const eventType = this.getEventType(event.keyCode);
|
|
410
|
+
if (!eventType) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
this.pressedKeys[eventType] = this.pressedKeys[eventType]
|
|
414
|
+
? this.pressedKeys[eventType] + 1
|
|
415
|
+
: 1;
|
|
416
|
+
event.preventDefault();
|
|
417
|
+
event.stopPropagation();
|
|
418
|
+
const details = {
|
|
419
|
+
pressedKeys: this.pressedKeys,
|
|
420
|
+
};
|
|
421
|
+
if (this.focusKey && this.getNodeLayoutByFocusKey(this.focusKey)) {
|
|
422
|
+
details.layout = this.getNodeLayoutByFocusKey(this.focusKey);
|
|
423
|
+
}
|
|
424
|
+
if (eventType === KEY_ENTER && this.focusKey) {
|
|
425
|
+
this.onEnterPress(details);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (eventType === KEY_BACK && this.focusKey) {
|
|
429
|
+
this.onBackPress(details);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const preventDefaultNavigation = this.onArrowPress(eventType, details) === false;
|
|
433
|
+
if (preventDefaultNavigation) {
|
|
434
|
+
this.log('keyDownEventListener', 'default navigation prevented');
|
|
435
|
+
this.visualDebugger?.clear();
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
this.onKeyEvent(event);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
this.keyDownEventListener = this.throttle
|
|
442
|
+
? throttle(keyDownHandler, this.throttle, THROTTLE_OPTIONS)
|
|
443
|
+
: keyDownHandler;
|
|
444
|
+
this.keyUpEventListener = (event) => {
|
|
445
|
+
const eventType = this.getEventType(event.keyCode);
|
|
446
|
+
if (eventType) {
|
|
447
|
+
delete this.pressedKeys[eventType];
|
|
448
|
+
}
|
|
449
|
+
if (this.throttle && !this.throttleKeypresses) {
|
|
450
|
+
// @ts-ignore
|
|
451
|
+
this.keyDownEventListener?.cancel?.();
|
|
452
|
+
}
|
|
453
|
+
if (eventType === KEY_ENTER && this.focusKey) {
|
|
454
|
+
this.onEnterRelease();
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
window.addEventListener('keyup', this.keyUpEventListener);
|
|
458
|
+
window.addEventListener('keydown', this.keyDownEventListener);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
unbindEventHandlers() {
|
|
462
|
+
if (typeof window !== 'undefined' && window.removeEventListener) {
|
|
463
|
+
window.removeEventListener('keydown', this.keyDownEventListener);
|
|
464
|
+
this.keyDownEventListener = null;
|
|
465
|
+
if (this.throttle) {
|
|
466
|
+
window.removeEventListener('keyup', this.keyUpEventListener);
|
|
467
|
+
this.keyUpEventListener = null;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
onEnterPress(details) {
|
|
472
|
+
const component = this.focusableComponents[this.focusKey];
|
|
473
|
+
if (!component) {
|
|
474
|
+
this.log('onEnterPress', 'noComponent');
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (!component.focusable) {
|
|
478
|
+
this.log('onEnterPress', 'componentNotFocusable');
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
component.onEnterPressHandler?.(details);
|
|
482
|
+
}
|
|
483
|
+
onBackPress(details) {
|
|
484
|
+
const component = this.focusableComponents[this.focusKey];
|
|
485
|
+
if (!component) {
|
|
486
|
+
this.log('onBackPress', 'noComponent');
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (!component.focusable) {
|
|
490
|
+
this.log('onBackPress', 'componentNotFocusable');
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
component.onBackPressHandler?.(details);
|
|
494
|
+
}
|
|
495
|
+
onEnterRelease() {
|
|
496
|
+
const component = this.focusableComponents[this.focusKey];
|
|
497
|
+
if (!component) {
|
|
498
|
+
this.log('onEnterRelease', 'noComponent');
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (!component.focusable) {
|
|
502
|
+
this.log('onEnterRelease', 'componentNotFocusable');
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
component.onEnterReleaseHandler?.();
|
|
506
|
+
}
|
|
507
|
+
onArrowPress(direction, details) {
|
|
508
|
+
const component = this.focusableComponents[this.focusKey];
|
|
509
|
+
if (!component) {
|
|
510
|
+
this.log('onArrowPress', 'noComponent');
|
|
511
|
+
return undefined;
|
|
512
|
+
}
|
|
513
|
+
return component.onArrowPressHandler?.(direction, details);
|
|
514
|
+
}
|
|
515
|
+
navigateByDirection(direction, details = {}) {
|
|
516
|
+
if (this.paused === true) {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const validDirections = [
|
|
520
|
+
DIRECTION_DOWN,
|
|
521
|
+
DIRECTION_UP,
|
|
522
|
+
DIRECTION_LEFT,
|
|
523
|
+
DIRECTION_RIGHT,
|
|
524
|
+
];
|
|
525
|
+
if (validDirections.includes(direction)) {
|
|
526
|
+
this.log('navigateByDirection', 'direction', direction);
|
|
527
|
+
this.smartNavigate(direction, null, details);
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
this.log('navigateByDirection', `Invalid direction. You passed: \`${direction}\`, but you can use only these: `, validDirections);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
onKeyEvent(event) {
|
|
534
|
+
this.visualDebugger?.clear();
|
|
535
|
+
const direction = Object.keys(this.getKeyMap()).find((key) => this.getKeyMap()[key].includes(event.keyCode));
|
|
536
|
+
if (direction) {
|
|
537
|
+
this.smartNavigate(direction, null, { event });
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
smartNavigate(direction, fromParentFocusKey, details) {
|
|
541
|
+
this.log('smartNavigate', 'direction', direction);
|
|
542
|
+
this.log('smartNavigate', 'fromParentFocusKey', fromParentFocusKey);
|
|
543
|
+
this.log('smartNavigate', 'this.focusKey', this.focusKey);
|
|
544
|
+
if (!this.nativeMode && !fromParentFocusKey) {
|
|
545
|
+
Object.values(this.focusableComponents).forEach((component) => {
|
|
546
|
+
component.layoutUpdated = false;
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
const currentComponent = this.focusableComponents[fromParentFocusKey || this.focusKey];
|
|
550
|
+
this.log('smartNavigate', 'currentComponent', currentComponent ? currentComponent.focusKey : undefined, currentComponent ? currentComponent.node : undefined);
|
|
551
|
+
if (currentComponent) {
|
|
552
|
+
this.updateLayout(currentComponent.focusKey);
|
|
553
|
+
const { parentFocusKey, focusKey, layout } = currentComponent;
|
|
554
|
+
const isVerticalDirection = direction === DIRECTION_DOWN || direction === DIRECTION_UP;
|
|
555
|
+
const isIncrementalDirection = direction === DIRECTION_DOWN || direction === DIRECTION_RIGHT;
|
|
556
|
+
const currentCutoffCoordinate = SpatialNavigation.getCutoffCoordinate(isVerticalDirection, isIncrementalDirection, false, layout);
|
|
557
|
+
/**
|
|
558
|
+
* Get only the siblings with the coords on the way of our moving direction
|
|
559
|
+
*/
|
|
560
|
+
const siblings = Object.values(this.focusableComponents).filter((component) => {
|
|
561
|
+
if (component.parentFocusKey === parentFocusKey &&
|
|
562
|
+
component.focusable) {
|
|
563
|
+
this.updateLayout(component.focusKey);
|
|
564
|
+
const siblingCutoffCoordinate = SpatialNavigation.getCutoffCoordinate(isVerticalDirection, isIncrementalDirection, true, component.layout);
|
|
565
|
+
return isIncrementalDirection
|
|
566
|
+
? siblingCutoffCoordinate >= currentCutoffCoordinate
|
|
567
|
+
: siblingCutoffCoordinate <= currentCutoffCoordinate;
|
|
568
|
+
}
|
|
569
|
+
return false;
|
|
570
|
+
});
|
|
571
|
+
if (this.debug) {
|
|
572
|
+
this.log('smartNavigate', 'siblings', siblings);
|
|
573
|
+
}
|
|
574
|
+
if (siblings.length > 0) {
|
|
575
|
+
const sortedSiblings = this.sortSiblingsByPriority(siblings, layout, direction, focusKey);
|
|
576
|
+
if (this.debug) {
|
|
577
|
+
this.log('smartNavigate', 'sortedSiblings', sortedSiblings);
|
|
578
|
+
}
|
|
579
|
+
const nextComponent = sortedSiblings[0];
|
|
580
|
+
if (nextComponent) {
|
|
581
|
+
this.setFocus(nextComponent.focusKey, details);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
this.log('smartNavigate', 'noSiblings');
|
|
586
|
+
if (parentFocusKey) {
|
|
587
|
+
this.smartNavigate(direction, parentFocusKey, details);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
saveLastFocusedChildKey(component, focusKey) {
|
|
593
|
+
if (component) {
|
|
594
|
+
this.log('saveLastFocusedChildKey', `${component.focusKey} lastFocusedChildKey set`, focusKey);
|
|
595
|
+
component.lastFocusedChildKey = focusKey;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
log(functionName, debugString, ...rest) {
|
|
599
|
+
if (this.debug) {
|
|
600
|
+
console.log(`%c${functionName}%c${debugString}`, `background: ${DEBUG_FN_COLORS[this.logIndex % DEBUG_FN_COLORS.length]}; color: black; padding: 1px 5px;`, 'background: #333; color: #BADA55; padding: 1px 5px;', ...rest);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
getNextFocusKey(targetFocusKey) {
|
|
604
|
+
this.log('getNextFocusKey', 'targetFocusKey', targetFocusKey);
|
|
605
|
+
const targetComponent = this.focusableComponents[targetFocusKey];
|
|
606
|
+
if (!targetComponent) {
|
|
607
|
+
return targetFocusKey;
|
|
608
|
+
}
|
|
609
|
+
const { trackChildren } = targetComponent;
|
|
610
|
+
if (!trackChildren) {
|
|
611
|
+
return targetFocusKey;
|
|
612
|
+
}
|
|
613
|
+
const children = Object.values(this.focusableComponents).filter((component) => component.parentFocusKey === targetFocusKey && component.focusable);
|
|
614
|
+
if (children.length === 0) {
|
|
615
|
+
return targetFocusKey;
|
|
616
|
+
}
|
|
617
|
+
const { lastFocusedChildKey, preferredChildFocusKey } = targetComponent;
|
|
618
|
+
/**
|
|
619
|
+
* First of all trying to focus last focused child
|
|
620
|
+
*/
|
|
621
|
+
if (lastFocusedChildKey &&
|
|
622
|
+
!targetComponent.forgetLastFocusedChild &&
|
|
623
|
+
this.isParticipatingFocusableComponent(lastFocusedChildKey)) {
|
|
624
|
+
this.log('getNextFocusKey', 'lastFocusedChildKey will be focused', lastFocusedChildKey);
|
|
625
|
+
return this.getNextFocusKey(lastFocusedChildKey);
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* If there is no lastFocusedChild, trying to focus the preferred focused key
|
|
629
|
+
*/
|
|
630
|
+
if (preferredChildFocusKey &&
|
|
631
|
+
this.isParticipatingFocusableComponent(preferredChildFocusKey)) {
|
|
632
|
+
this.log('getNextFocusKey', 'preferredChildFocusKey will be focused', preferredChildFocusKey);
|
|
633
|
+
return this.getNextFocusKey(preferredChildFocusKey);
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Otherwise, trying to focus something by coordinates
|
|
637
|
+
*/
|
|
638
|
+
children.forEach((component) => this.updateLayout(component.focusKey));
|
|
639
|
+
const { focusKey: childKey } = getChildClosestToOrigin(children);
|
|
640
|
+
this.log('getNextFocusKey', 'childKey will be focused', childKey);
|
|
641
|
+
return this.getNextFocusKey(childKey);
|
|
642
|
+
}
|
|
643
|
+
addFocusable({ focusKey, node, parentFocusKey, onEnterPressHandler, onEnterReleaseHandler, onBackPressHandler, onArrowPressHandler, onBecameFocusedHandler, onBecameBlurredHandler, forgetLastFocusedChild, trackChildren, onUpdateFocus, onUpdateHasFocusedChild, preferredChildFocusKey, autoRestoreFocus, focusable = true, blockNavigationOut, }) {
|
|
644
|
+
this.focusableComponents[focusKey] = {
|
|
645
|
+
focusKey,
|
|
646
|
+
node,
|
|
647
|
+
parentFocusKey,
|
|
648
|
+
onEnterPressHandler,
|
|
649
|
+
onEnterReleaseHandler,
|
|
650
|
+
onBackPressHandler,
|
|
651
|
+
onArrowPressHandler,
|
|
652
|
+
onBecameFocusedHandler,
|
|
653
|
+
onBecameBlurredHandler,
|
|
654
|
+
onUpdateFocus,
|
|
655
|
+
onUpdateHasFocusedChild,
|
|
656
|
+
forgetLastFocusedChild,
|
|
657
|
+
trackChildren,
|
|
658
|
+
lastFocusedChildKey: null,
|
|
659
|
+
preferredChildFocusKey,
|
|
660
|
+
focusable,
|
|
661
|
+
blockNavigationOut,
|
|
662
|
+
autoRestoreFocus,
|
|
663
|
+
layout: {
|
|
664
|
+
x: 0,
|
|
665
|
+
y: 0,
|
|
666
|
+
width: 0,
|
|
667
|
+
height: 0,
|
|
668
|
+
left: 0,
|
|
669
|
+
top: 0,
|
|
670
|
+
node,
|
|
671
|
+
},
|
|
672
|
+
layoutUpdated: false,
|
|
673
|
+
};
|
|
674
|
+
if (this.nativeMode) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
this.updateLayout(focusKey);
|
|
678
|
+
/**
|
|
679
|
+
* If for some reason this component was already focused before it was added, call the update
|
|
680
|
+
*/
|
|
681
|
+
if (focusKey === this.focusKey) {
|
|
682
|
+
this.setFocus(focusKey);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
removeFocusable({ focusKey }) {
|
|
686
|
+
const componentToRemove = this.focusableComponents[focusKey];
|
|
687
|
+
if (componentToRemove) {
|
|
688
|
+
const { parentFocusKey } = componentToRemove;
|
|
689
|
+
Reflect.deleteProperty(this.focusableComponents, focusKey);
|
|
690
|
+
const parentComponent = this.focusableComponents[parentFocusKey];
|
|
691
|
+
const isFocused = focusKey === this.focusKey;
|
|
692
|
+
/**
|
|
693
|
+
* If the component was stored as lastFocusedChild, clear lastFocusedChildKey from parent
|
|
694
|
+
*/
|
|
695
|
+
parentComponent &&
|
|
696
|
+
parentComponent.lastFocusedChildKey === focusKey &&
|
|
697
|
+
(parentComponent.lastFocusedChildKey = null);
|
|
698
|
+
if (this.nativeMode) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* If the component was also focused at this time, focus another one
|
|
703
|
+
*/
|
|
704
|
+
if (isFocused && parentComponent && parentComponent.autoRestoreFocus) {
|
|
705
|
+
this.setFocus(parentFocusKey);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
getNodeLayoutByFocusKey(focusKey) {
|
|
710
|
+
const component = this.focusableComponents[focusKey];
|
|
711
|
+
if (component) {
|
|
712
|
+
this.updateLayout(component.focusKey);
|
|
713
|
+
return component.layout;
|
|
714
|
+
}
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
setCurrentFocusedKey(newFocusKey, details) {
|
|
718
|
+
if (this.isFocusableComponent(this.focusKey) &&
|
|
719
|
+
newFocusKey !== this.focusKey) {
|
|
720
|
+
const oldComponent = this.focusableComponents[this.focusKey];
|
|
721
|
+
const parentComponent = this.focusableComponents[oldComponent.parentFocusKey];
|
|
722
|
+
this.saveLastFocusedChildKey(parentComponent, this.focusKey);
|
|
723
|
+
oldComponent.onUpdateFocus?.(false);
|
|
724
|
+
oldComponent.onBecameBlurredHandler?.(this.getNodeLayoutByFocusKey(this.focusKey), details);
|
|
725
|
+
}
|
|
726
|
+
this.focusKey = newFocusKey;
|
|
727
|
+
if (this.isFocusableComponent(this.focusKey)) {
|
|
728
|
+
const newComponent = this.focusableComponents[this.focusKey];
|
|
729
|
+
newComponent.onUpdateFocus?.(true);
|
|
730
|
+
newComponent.onBecameFocusedHandler?.(this.getNodeLayoutByFocusKey(this.focusKey), details);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
updateParentsHasFocusedChild(focusKey, details) {
|
|
734
|
+
const parents = [];
|
|
735
|
+
let currentComponent = this.focusableComponents[focusKey];
|
|
736
|
+
/**
|
|
737
|
+
* Recursively iterate the tree up and find all the parents' focus keys
|
|
738
|
+
*/
|
|
739
|
+
while (currentComponent) {
|
|
740
|
+
const { parentFocusKey } = currentComponent;
|
|
741
|
+
const parentComponent = this.focusableComponents[parentFocusKey];
|
|
742
|
+
if (parentComponent) {
|
|
743
|
+
const { focusKey: currentParentFocusKey } = parentComponent;
|
|
744
|
+
parents.push(currentParentFocusKey);
|
|
745
|
+
}
|
|
746
|
+
currentComponent = parentComponent;
|
|
747
|
+
}
|
|
748
|
+
const parentsToRemoveFlag = difference(this.parentsHavingFocusedChild, parents);
|
|
749
|
+
const parentsToAddFlag = difference(parents, this.parentsHavingFocusedChild);
|
|
750
|
+
forEach(parentsToRemoveFlag, (parentFocusKey) => {
|
|
751
|
+
const parentComponent = this.focusableComponents[parentFocusKey];
|
|
752
|
+
parentComponent &&
|
|
753
|
+
parentComponent.trackChildren &&
|
|
754
|
+
parentComponent.onUpdateHasFocusedChild?.(false);
|
|
755
|
+
this.onIntermediateNodeBecameBlurred(parentFocusKey, details);
|
|
756
|
+
});
|
|
757
|
+
forEach(parentsToAddFlag, (parentFocusKey) => {
|
|
758
|
+
const parentComponent = this.focusableComponents[parentFocusKey];
|
|
759
|
+
parentComponent &&
|
|
760
|
+
parentComponent.trackChildren &&
|
|
761
|
+
parentComponent.onUpdateHasFocusedChild?.(true);
|
|
762
|
+
this.onIntermediateNodeBecameFocused(parentFocusKey, details);
|
|
763
|
+
});
|
|
764
|
+
this.parentsHavingFocusedChild = parents;
|
|
765
|
+
}
|
|
766
|
+
updateParentsLastFocusedChild(focusKey) {
|
|
767
|
+
let currentComponent = this.focusableComponents[focusKey];
|
|
768
|
+
/**
|
|
769
|
+
* Recursively iterate the tree up and update all the parent's lastFocusedChild
|
|
770
|
+
*/
|
|
771
|
+
while (currentComponent) {
|
|
772
|
+
const { parentFocusKey } = currentComponent;
|
|
773
|
+
const parentComponent = this.focusableComponents[parentFocusKey];
|
|
774
|
+
if (parentComponent) {
|
|
775
|
+
this.saveLastFocusedChildKey(parentComponent, currentComponent.focusKey);
|
|
776
|
+
}
|
|
777
|
+
currentComponent = parentComponent;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
getKeyMap() {
|
|
781
|
+
return this.keyMap;
|
|
782
|
+
}
|
|
783
|
+
setKeyMap(keyMap) {
|
|
784
|
+
this.keyMap = {
|
|
785
|
+
...this.getKeyMap(),
|
|
786
|
+
...keyMap,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
isFocusableComponent(focusKey) {
|
|
790
|
+
return !!this.focusableComponents[focusKey];
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Checks whether the focusableComponent is actually participating in spatial navigation (in other words, is a
|
|
794
|
+
* 'focusable' focusableComponent). Seems less confusing than calling it isFocusableFocusableComponent()
|
|
795
|
+
*/
|
|
796
|
+
isParticipatingFocusableComponent(focusKey) {
|
|
797
|
+
return (this.isFocusableComponent(focusKey) &&
|
|
798
|
+
this.focusableComponents[focusKey].focusable);
|
|
799
|
+
}
|
|
800
|
+
onIntermediateNodeBecameFocused(focusKey, details) {
|
|
801
|
+
this.isParticipatingFocusableComponent(focusKey) &&
|
|
802
|
+
this.focusableComponents[focusKey].onBecameFocusedHandler?.(this.getNodeLayoutByFocusKey(focusKey), details);
|
|
803
|
+
}
|
|
804
|
+
onIntermediateNodeBecameBlurred(focusKey, details) {
|
|
805
|
+
this.isParticipatingFocusableComponent(focusKey) &&
|
|
806
|
+
this.focusableComponents[focusKey].onBecameBlurredHandler?.(this.getNodeLayoutByFocusKey(focusKey), details);
|
|
807
|
+
}
|
|
808
|
+
pause() {
|
|
809
|
+
this.paused = true;
|
|
810
|
+
}
|
|
811
|
+
resume() {
|
|
812
|
+
this.paused = false;
|
|
813
|
+
}
|
|
814
|
+
setFocus(targetFocusKey, details = {}) {
|
|
815
|
+
const lastFocusedKey = this.focusKey;
|
|
816
|
+
const newFocusKey = this.getNextFocusKey(targetFocusKey);
|
|
817
|
+
this.log('setFocus', 'newFocusKey', newFocusKey);
|
|
818
|
+
this.setCurrentFocusedKey(newFocusKey, details);
|
|
819
|
+
this.updateParentsHasFocusedChild(newFocusKey, details);
|
|
820
|
+
this.updateParentsLastFocusedChild(lastFocusedKey);
|
|
821
|
+
}
|
|
822
|
+
updateAllLayouts() {
|
|
823
|
+
if (this.nativeMode) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
Object.entries(this.focusableComponents).forEach(([focusKey]) => {
|
|
827
|
+
this.updateLayout(focusKey);
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
updateLayout(focusKey) {
|
|
831
|
+
const component = this.focusableComponents[focusKey];
|
|
832
|
+
if (!component || this.nativeMode || component.layoutUpdated) {
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
const { node } = component;
|
|
836
|
+
measureLayout(node, (x, y, width, height, left, top) => {
|
|
837
|
+
component.layout = {
|
|
838
|
+
x,
|
|
839
|
+
y,
|
|
840
|
+
width,
|
|
841
|
+
height,
|
|
842
|
+
left,
|
|
843
|
+
top,
|
|
844
|
+
node,
|
|
845
|
+
};
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
updateFocusable(focusKey, { node, preferredChildFocusKey, focusable, blockNavigationOut, }) {
|
|
849
|
+
if (this.nativeMode) {
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const component = this.focusableComponents[focusKey];
|
|
853
|
+
if (component) {
|
|
854
|
+
component.preferredChildFocusKey = preferredChildFocusKey;
|
|
855
|
+
component.focusable = focusable ?? component.focusable;
|
|
856
|
+
component.blockNavigationOut = blockNavigationOut;
|
|
857
|
+
if (node) {
|
|
858
|
+
component.node = node;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
isNativeMode() {
|
|
863
|
+
return this.nativeMode;
|
|
864
|
+
}
|
|
865
|
+
getFocusableComponents() {
|
|
866
|
+
return Object.values(this.focusableComponents);
|
|
867
|
+
}
|
|
868
|
+
getFocusableComponentsByParentFocusKey(parentFocusKey) {
|
|
869
|
+
return Object.values(this.focusableComponents).filter((component) => component.parentFocusKey === parentFocusKey);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Export singleton
|
|
874
|
+
*/
|
|
875
|
+
export default new SpatialNavigation();
|