@lightningtv/solid 3.0.0-17 → 3.0.0-18
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/Column.jsx +8 -7
- package/dist/src/primitives/Column.jsx.map +1 -1
- package/dist/src/primitives/KeepAlive.d.ts +4 -3
- package/dist/src/primitives/KeepAlive.jsx +70 -9
- package/dist/src/primitives/KeepAlive.jsx.map +1 -1
- package/dist/src/primitives/Lazy.d.ts +6 -7
- package/dist/src/primitives/Lazy.jsx +23 -20
- package/dist/src/primitives/Lazy.jsx.map +1 -1
- package/dist/src/primitives/Row.jsx +8 -7
- package/dist/src/primitives/Row.jsx.map +1 -1
- package/dist/src/primitives/Virtual.d.ts +18 -0
- package/dist/src/primitives/Virtual.jsx +426 -0
- package/dist/src/primitives/Virtual.jsx.map +1 -0
- package/dist/src/primitives/VirtualGrid.d.ts +13 -0
- package/dist/src/primitives/VirtualGrid.jsx +139 -0
- package/dist/src/primitives/VirtualGrid.jsx.map +1 -0
- package/dist/src/primitives/VirtualList.d.ts +11 -0
- package/dist/src/primitives/VirtualList.jsx +96 -0
- package/dist/src/primitives/VirtualList.jsx.map +1 -0
- package/dist/src/primitives/VirtualRow.d.ts +13 -0
- package/dist/src/primitives/VirtualRow.jsx +97 -0
- package/dist/src/primitives/VirtualRow.jsx.map +1 -0
- package/dist/src/primitives/Visible.d.ts +0 -1
- package/dist/src/primitives/Visible.jsx +1 -1
- package/dist/src/primitives/Visible.jsx.map +1 -1
- package/dist/src/primitives/index.d.ts +4 -1
- package/dist/src/primitives/index.js +4 -1
- package/dist/src/primitives/index.js.map +1 -1
- package/dist/src/primitives/types.d.ts +1 -0
- package/dist/src/primitives/utils/handleNavigation.d.ts +0 -1
- package/dist/src/primitives/utils/handleNavigation.js +7 -5
- package/dist/src/primitives/utils/handleNavigation.js.map +1 -1
- package/dist/src/primitives/utils/withScrolling.d.ts +5 -1
- package/dist/src/primitives/utils/withScrolling.js +8 -3
- package/dist/src/primitives/utils/withScrolling.js.map +1 -1
- package/dist/src/render.d.ts +2 -1
- package/dist/src/render.js +4 -0
- package/dist/src/render.js.map +1 -1
- package/dist/src/universal.d.ts +25 -0
- package/dist/src/universal.js +232 -0
- package/dist/src/universal.js.map +1 -0
- package/dist/src/utils.d.ts +2 -0
- package/dist/src/utils.js +8 -0
- package/dist/src/utils.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -3
- package/src/primitives/Column.tsx +10 -9
- package/src/primitives/Lazy.tsx +34 -38
- package/src/primitives/Row.tsx +11 -9
- package/src/primitives/Virtual.tsx +469 -0
- package/src/primitives/VirtualGrid.tsx +199 -0
- package/src/primitives/Visible.tsx +1 -2
- package/src/primitives/index.ts +8 -1
- package/src/primitives/types.ts +1 -0
- package/src/primitives/utils/handleNavigation.ts +8 -5
- package/src/primitives/utils/withScrolling.ts +20 -13
- package/src/render.ts +5 -0
- package/src/utils.ts +10 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import * as s from 'solid-js';
|
|
2
|
+
import * as lng from '@lightningtv/solid';
|
|
3
|
+
import * as lngp from '@lightningtv/solid/primitives';
|
|
4
|
+
import { List } from '@solid-primitives/list';
|
|
5
|
+
import * as utils from '../utils.js';
|
|
6
|
+
|
|
7
|
+
const columnScroll = lngp.withScrolling(false);
|
|
8
|
+
|
|
9
|
+
const rowStyles: lng.NodeStyles = {
|
|
10
|
+
display: 'flex',
|
|
11
|
+
flexWrap: 'wrap',
|
|
12
|
+
transition: {
|
|
13
|
+
y: true,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type VirtualGridProps<T> = lng.NewOmit<lngp.RowProps, 'children'> & {
|
|
18
|
+
each: readonly T[] | undefined | null | false;
|
|
19
|
+
columns: number; // items per row
|
|
20
|
+
rows?: number; // number of visible rows (default: 1)
|
|
21
|
+
buffer?: number;
|
|
22
|
+
onEndReached?: () => void;
|
|
23
|
+
onEndReachedThreshold?: number;
|
|
24
|
+
children: (item: s.Accessor<T>, index: s.Accessor<number>) => s.JSX.Element;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function VirtualGrid<T>(props: VirtualGridProps<T>): s.JSX.Element {
|
|
28
|
+
const bufferSize = () => props.buffer ?? 2;
|
|
29
|
+
const [ cursor, setCursor ] = s.createSignal(props.selected ?? 0);
|
|
30
|
+
const items = s.createMemo(() => props.each || []);
|
|
31
|
+
const itemsPerRow = () => props.columns;
|
|
32
|
+
const numberOfRows = () => props.rows ?? 1;
|
|
33
|
+
const totalVisibleItems = () => itemsPerRow() * numberOfRows();
|
|
34
|
+
|
|
35
|
+
const start = s.createMemo(() => {
|
|
36
|
+
const perRow = itemsPerRow();
|
|
37
|
+
const newRowIndex = Math.floor(cursor() / perRow);
|
|
38
|
+
const rawStart = newRowIndex * perRow - bufferSize() * perRow;
|
|
39
|
+
return Math.max(0, rawStart);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const end = s.createMemo(() => {
|
|
43
|
+
const perRow = itemsPerRow();
|
|
44
|
+
const newRowIndex = Math.floor(cursor() / perRow);
|
|
45
|
+
const rawEnd = (newRowIndex + bufferSize()) * perRow + totalVisibleItems();
|
|
46
|
+
return Math.min(items().length, rawEnd);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const [slice, setSlice] = s.createSignal(items().slice(start(), end()));
|
|
50
|
+
|
|
51
|
+
let viewRef!: lngp.NavigableElement;
|
|
52
|
+
|
|
53
|
+
function onVerticalNav(dir: -1 | 1): lngp.KeyHandler {
|
|
54
|
+
return function () {
|
|
55
|
+
const perRow = itemsPerRow();
|
|
56
|
+
const currentRowIndex = Math.floor(cursor() / perRow);
|
|
57
|
+
const maxRows = Math.floor(items().length / perRow);
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
currentRowIndex === 0 && dir === -1
|
|
61
|
+
|| currentRowIndex === maxRows && dir === 1
|
|
62
|
+
) return;
|
|
63
|
+
|
|
64
|
+
const selected = this.selected || 0;
|
|
65
|
+
const offset = dir * perRow;
|
|
66
|
+
const newIndex = utils.clamp(selected + offset, 0, items().length - 1);
|
|
67
|
+
const lastIdx = selected;
|
|
68
|
+
this.selected = newIndex;
|
|
69
|
+
const active = this.children[this.selected];
|
|
70
|
+
|
|
71
|
+
if (active instanceof lng.ElementNode) {
|
|
72
|
+
active.setFocus();
|
|
73
|
+
chainedOnSelectedChanged.call(
|
|
74
|
+
this as lngp.NavigableElement,
|
|
75
|
+
this.selected,
|
|
76
|
+
this as lngp.NavigableElement,
|
|
77
|
+
active,
|
|
78
|
+
lastIdx
|
|
79
|
+
);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const onUp = onVerticalNav(-1);
|
|
86
|
+
const onDown = onVerticalNav(1);
|
|
87
|
+
|
|
88
|
+
const onSelectedChanged: lngp.OnSelectedChanged = function (_idx, elm, active, _lastIdx,) {
|
|
89
|
+
let idx = _idx;
|
|
90
|
+
let lastIdx = _lastIdx;
|
|
91
|
+
const perRow = itemsPerRow();
|
|
92
|
+
const newRowIndex = Math.floor(idx / perRow);
|
|
93
|
+
const prevRowIndex = Math.floor((lastIdx || 0) / perRow);
|
|
94
|
+
const prevStart = start();
|
|
95
|
+
|
|
96
|
+
setCursor(prevStart + idx);
|
|
97
|
+
if (newRowIndex === prevRowIndex) return;
|
|
98
|
+
|
|
99
|
+
setSlice(items().slice(start(), end()));
|
|
100
|
+
|
|
101
|
+
// this.selected is relative to the slice
|
|
102
|
+
// and it doesn't get corrected automatically after children change
|
|
103
|
+
const idxCorrection = prevStart - start();
|
|
104
|
+
if (lastIdx) lastIdx += idxCorrection;
|
|
105
|
+
idx += idxCorrection;
|
|
106
|
+
this.selected += idxCorrection;
|
|
107
|
+
|
|
108
|
+
if (props.onEndReachedThreshold !== undefined && cursor() >= items().length - props.onEndReachedThreshold) {
|
|
109
|
+
props.onEndReached?.();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
queueMicrotask(() => {
|
|
113
|
+
const prevRowY = this.y + active.y;
|
|
114
|
+
this.updateLayout();
|
|
115
|
+
this.lng.y = prevRowY - active.y;
|
|
116
|
+
columnScroll(idx, elm, active, lastIdx);
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const chainedOnSelectedChanged = lngp.chainFunctions(props.onSelectedChanged, onSelectedChanged)!;
|
|
121
|
+
|
|
122
|
+
let cachedSelected: number | undefined;
|
|
123
|
+
const updateSelected = ([selected, _items]: [number?, any?]) => {
|
|
124
|
+
if (!viewRef || selected == null) return;
|
|
125
|
+
|
|
126
|
+
if (cachedSelected !== undefined) {
|
|
127
|
+
selected = cachedSelected;
|
|
128
|
+
cachedSelected = undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (selected >= items().length && props.onEndReached) {
|
|
132
|
+
props.onEndReached?.();
|
|
133
|
+
cachedSelected = selected;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const item = items()[selected];
|
|
138
|
+
let active = viewRef.children.find(x => x.item === item);
|
|
139
|
+
const lastSelected = viewRef.selected;
|
|
140
|
+
|
|
141
|
+
if (active instanceof lng.ElementNode) {
|
|
142
|
+
viewRef.selected = viewRef.children.indexOf(active);
|
|
143
|
+
active.setFocus();
|
|
144
|
+
chainedOnSelectedChanged.call(viewRef, viewRef.selected, viewRef, active, lastSelected);
|
|
145
|
+
} else {
|
|
146
|
+
setCursor(selected);
|
|
147
|
+
setSlice(items().slice(start(), end()));
|
|
148
|
+
|
|
149
|
+
queueMicrotask(() => {
|
|
150
|
+
viewRef.updateLayout();
|
|
151
|
+
active = viewRef.children.find(x => x.item === item);
|
|
152
|
+
if (active instanceof lng.ElementNode) {
|
|
153
|
+
viewRef.selected = viewRef.children.indexOf(active);
|
|
154
|
+
active.setFocus();
|
|
155
|
+
chainedOnSelectedChanged.call(viewRef, viewRef.selected, viewRef, active, lastSelected);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const scrollToIndex = (index: number) => {
|
|
162
|
+
s.untrack(() => updateSelected([index]));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
s.createEffect(s.on([() => props.selected, items], updateSelected));
|
|
166
|
+
|
|
167
|
+
s.createEffect(
|
|
168
|
+
s.on(items, () => {
|
|
169
|
+
if (!viewRef) return;
|
|
170
|
+
if (cachedSelected !== undefined) {
|
|
171
|
+
updateSelected([cachedSelected]);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
setSlice(items().slice(start(), end()));
|
|
175
|
+
}, { defer: true })
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<view
|
|
181
|
+
{...props}
|
|
182
|
+
scroll={props.scroll || 'always'}
|
|
183
|
+
ref={lngp.chainRefs(el => { viewRef = el as lngp.NavigableElement; }, props.ref)}
|
|
184
|
+
selected={props.selected || 0}
|
|
185
|
+
cursor={cursor()}
|
|
186
|
+
onLeft={/* @once */ lngp.chainFunctions(props.onLeft, lngp.navigableHandleNavigation)}
|
|
187
|
+
onRight={/* @once */ lngp.chainFunctions(props.onRight, lngp.navigableHandleNavigation)}
|
|
188
|
+
onUp={/* @once */ lngp.chainFunctions(props.onUp, onUp)}
|
|
189
|
+
onDown={/* @once */ lngp.chainFunctions(props.onDown, onDown)}
|
|
190
|
+
forwardFocus={/* @once */ lngp.navigableForwardFocus}
|
|
191
|
+
onCreate={/* @once */ props.selected ? lngp.chainFunctions(props.onCreate, columnScroll) : props.onCreate}
|
|
192
|
+
scrollToIndex={/* @once */ scrollToIndex}
|
|
193
|
+
onSelectedChanged={/* @once */ chainedOnSelectedChanged}
|
|
194
|
+
style={/* @once */ lng.combineStyles(props.style, rowStyles)}
|
|
195
|
+
>
|
|
196
|
+
<List each={slice()}>{props.children}</List>
|
|
197
|
+
</view>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
@@ -13,7 +13,6 @@ import { ElementNode } from '@lightningtv/solid';
|
|
|
13
13
|
export function Visible<T>(props: {
|
|
14
14
|
when: T | undefined | null | false;
|
|
15
15
|
keyed?: boolean;
|
|
16
|
-
fallback?: JSX.Element;
|
|
17
16
|
children: JSX.Element;
|
|
18
17
|
}): JSX.Element {
|
|
19
18
|
let child: ChildrenReturn | undefined;
|
|
@@ -55,6 +54,6 @@ export function Visible<T>(props: {
|
|
|
55
54
|
}
|
|
56
55
|
});
|
|
57
56
|
|
|
58
|
-
return c ? child :
|
|
57
|
+
return c || child ? child : null;
|
|
59
58
|
}) as unknown as JSX.Element;
|
|
60
59
|
};
|
package/src/primitives/index.ts
CHANGED
|
@@ -16,7 +16,14 @@ export * from './Suspense.jsx';
|
|
|
16
16
|
export * from './Marquee.jsx';
|
|
17
17
|
export * from './createFocusStack.jsx';
|
|
18
18
|
export * from './useHold.js';
|
|
19
|
-
export {
|
|
19
|
+
export {
|
|
20
|
+
withScrolling,
|
|
21
|
+
scrollColumn,
|
|
22
|
+
scrollRow,
|
|
23
|
+
} from './utils/withScrolling.js';
|
|
24
|
+
export * from './VirtualGrid.jsx';
|
|
25
|
+
export * from './Virtual.jsx';
|
|
26
|
+
export * from './utils/withScrolling.js';
|
|
20
27
|
export {
|
|
21
28
|
type AnyFunction,
|
|
22
29
|
chainFunctions,
|
package/src/primitives/types.ts
CHANGED
|
@@ -54,6 +54,7 @@ export interface NavigableProps extends NodeProps {
|
|
|
54
54
|
// @ts-expect-error animationSettings is not identical - weird
|
|
55
55
|
export interface NavigableElement extends ElementNode, NavigableProps {
|
|
56
56
|
selected: number;
|
|
57
|
+
scrollToIndex: (this: NavigableElement, index: number) => void;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
export interface NavigableStyleProperties {
|
|
@@ -24,7 +24,7 @@ function findFirstFocusableChildIdx(
|
|
|
24
24
|
i = (i + el.children.length) % el.children.length;
|
|
25
25
|
} else break;
|
|
26
26
|
}
|
|
27
|
-
if (!el.children[i]
|
|
27
|
+
if (!el.children[i]?.skipFocus) {
|
|
28
28
|
return i;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -41,12 +41,14 @@ function selectChild(el: lngp.NavigableElement, index: number): boolean {
|
|
|
41
41
|
|
|
42
42
|
const lastSelected = el.selected;
|
|
43
43
|
el.selected = index;
|
|
44
|
-
child.setFocus();
|
|
45
44
|
|
|
46
|
-
if (
|
|
47
|
-
|
|
45
|
+
if (!lng.isFocused(child)) {
|
|
46
|
+
child.setFocus();
|
|
48
47
|
}
|
|
49
48
|
|
|
49
|
+
// Always call onSelectedChanged on first focus for clients
|
|
50
|
+
el.onSelectedChanged?.(index, el, child as lng.ElementNode, lastSelected);
|
|
51
|
+
|
|
50
52
|
return true;
|
|
51
53
|
}
|
|
52
54
|
|
|
@@ -90,10 +92,11 @@ export const navigableForwardFocus: lng.ForwardFocusHandler = function () {
|
|
|
90
92
|
let selected = navigable.selected;
|
|
91
93
|
selected = idxInArray(selected, this.children) ? selected : 0;
|
|
92
94
|
selected = findFirstFocusableChildIdx(navigable, selected);
|
|
95
|
+
// update selected as firstfocusable maybe different if first element has skipFocus
|
|
96
|
+
navigable.selected = selected;
|
|
93
97
|
return selectChild(navigable, selected);
|
|
94
98
|
};
|
|
95
99
|
|
|
96
|
-
/** @deprecated Use {@link navigableHandleNavigation} instead */
|
|
97
100
|
export function handleNavigation(
|
|
98
101
|
direction: 'up' | 'right' | 'down' | 'left',
|
|
99
102
|
): lng.KeyHandler {
|
|
@@ -5,6 +5,13 @@ import type {
|
|
|
5
5
|
Styles,
|
|
6
6
|
} from '@lightningtv/core';
|
|
7
7
|
|
|
8
|
+
export type Scroller = (
|
|
9
|
+
selected: number | ElementNode,
|
|
10
|
+
component?: ElementNode,
|
|
11
|
+
selectedElement?: ElementNode | ElementText,
|
|
12
|
+
lastSelected?: number,
|
|
13
|
+
) => void;
|
|
14
|
+
|
|
8
15
|
// Adds properties expected by withScrolling
|
|
9
16
|
export interface ScrollableElement extends ElementNode {
|
|
10
17
|
scrollIndex?: number;
|
|
@@ -33,16 +40,12 @@ const isNotShown = (node: ElementNode | ElementText) => {
|
|
|
33
40
|
Always scroll moves the list every time
|
|
34
41
|
*/
|
|
35
42
|
|
|
36
|
-
|
|
43
|
+
/** Use {@link scrollRow} or {@link scrollColumn} */
|
|
44
|
+
export function withScrolling(isRow: boolean): Scroller {
|
|
37
45
|
const dimension = isRow ? 'width' : 'height';
|
|
38
46
|
const axis = isRow ? 'x' : 'y';
|
|
39
47
|
|
|
40
|
-
return (
|
|
41
|
-
selected: number | ElementNode,
|
|
42
|
-
component?: ElementNode,
|
|
43
|
-
selectedElement?: ElementNode | ElementText,
|
|
44
|
-
lastSelected?: number,
|
|
45
|
-
) => {
|
|
48
|
+
return (selected, component, selectedElement, lastSelected) => {
|
|
46
49
|
let componentRef = component as ScrollableElement;
|
|
47
50
|
if (typeof selected !== 'number') {
|
|
48
51
|
componentRef = selected as ScrollableElement;
|
|
@@ -51,6 +54,7 @@ export function withScrolling(isRow: boolean) {
|
|
|
51
54
|
if (
|
|
52
55
|
!componentRef ||
|
|
53
56
|
componentRef.scroll === 'none' ||
|
|
57
|
+
selected === lastSelected ||
|
|
54
58
|
!componentRef.children.length
|
|
55
59
|
)
|
|
56
60
|
return;
|
|
@@ -60,7 +64,7 @@ export function withScrolling(isRow: boolean) {
|
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
const lng = componentRef.lng as unknown as INode;
|
|
63
|
-
const screenSize = isRow ? lng.stage.root.
|
|
67
|
+
const screenSize = isRow ? lng.stage.root.w : lng.stage.root.h;
|
|
64
68
|
// Determine if movement is incremental or decremental
|
|
65
69
|
const isIncrementing =
|
|
66
70
|
lastSelected === undefined || lastSelected - 1 !== selected;
|
|
@@ -90,10 +94,9 @@ export function withScrolling(isRow: boolean) {
|
|
|
90
94
|
|
|
91
95
|
// Allows manual position control
|
|
92
96
|
const targetPosition = componentRef._targetPosition ?? componentRef[axis];
|
|
93
|
-
const rootPosition =
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
: Math.max(targetPosition, componentRef[axis]);
|
|
97
|
+
const rootPosition = isIncrementing
|
|
98
|
+
? Math.min(targetPosition, componentRef[axis])
|
|
99
|
+
: Math.max(targetPosition, componentRef[axis]);
|
|
97
100
|
componentRef.offset = componentRef.offset ?? rootPosition;
|
|
98
101
|
const offset = componentRef.offset;
|
|
99
102
|
selectedElement =
|
|
@@ -156,7 +159,8 @@ export function withScrolling(isRow: boolean) {
|
|
|
156
159
|
nextPosition = rootPosition + selectedSize + gap;
|
|
157
160
|
}
|
|
158
161
|
} else if (isIncrementing) {
|
|
159
|
-
nextPosition = -selectedPosition + offset;
|
|
162
|
+
//nextPosition = -selectedPosition + offset;
|
|
163
|
+
nextPosition = rootPosition - selectedSize - gap;
|
|
160
164
|
} else {
|
|
161
165
|
nextPosition = rootPosition + selectedSize + gap;
|
|
162
166
|
}
|
|
@@ -186,3 +190,6 @@ export function withScrolling(isRow: boolean) {
|
|
|
186
190
|
}
|
|
187
191
|
};
|
|
188
192
|
}
|
|
193
|
+
|
|
194
|
+
export const scrollRow = /* @__PURE__ */ withScrolling(true);
|
|
195
|
+
export const scrollColumn = /* @__PURE__ */ withScrolling(false);
|
package/src/render.ts
CHANGED
|
@@ -156,3 +156,8 @@ export const Text = (props: TextProps) => {
|
|
|
156
156
|
spread(el, props, false);
|
|
157
157
|
return el as unknown as JSXElement;
|
|
158
158
|
};
|
|
159
|
+
|
|
160
|
+
export function registerDefaultShader(name: string, shader: any) {
|
|
161
|
+
// noop for v2
|
|
162
|
+
// renderer.stage.shManager.registerShaderType('rounded', Rounded);
|
|
163
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -61,3 +61,13 @@ export function combineStylesMemo<T extends Styles>(
|
|
|
61
61
|
...style1,
|
|
62
62
|
}));
|
|
63
63
|
}
|
|
64
|
+
|
|
65
|
+
export const clamp = (value: number, min: number, max: number) =>
|
|
66
|
+
min < max
|
|
67
|
+
? Math.min(Math.max(value, min), max)
|
|
68
|
+
: Math.min(Math.max(value, max), min);
|
|
69
|
+
|
|
70
|
+
export function mod(n: number, m: number): number {
|
|
71
|
+
if (m === 0) return 0;
|
|
72
|
+
return ((n % m) + m) % m;
|
|
73
|
+
}
|