@ng-native/components 0.0.1 → 0.1.0
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/LICENSE +21 -0
- package/README.md +78 -1
- package/dist/activity-indicator.d.ts +41 -0
- package/dist/activity-indicator.js +66 -0
- package/dist/activity-indicator.js.map +1 -0
- package/dist/animated-style.d.ts +31 -0
- package/dist/animated-style.js +102 -0
- package/dist/animated-style.js.map +1 -0
- package/dist/animated.d.ts +125 -0
- package/dist/animated.js +538 -0
- package/dist/animated.js.map +1 -0
- package/dist/animation.d.ts +40 -0
- package/dist/animation.js +20 -0
- package/dist/animation.js.map +1 -0
- package/dist/animations-web.d.ts +8 -0
- package/dist/animations-web.js +31 -0
- package/dist/animations-web.js.map +1 -0
- package/dist/animations.d.ts +7 -0
- package/dist/animations.js +58 -0
- package/dist/animations.js.map +1 -0
- package/dist/controlled-model.d.ts +33 -0
- package/dist/controlled-model.js +47 -0
- package/dist/controlled-model.js.map +1 -0
- package/dist/easing.d.ts +21 -0
- package/dist/easing.js +50 -0
- package/dist/easing.js.map +1 -0
- package/dist/events.d.ts +169 -0
- package/dist/events.js +2 -0
- package/dist/events.js.map +1 -0
- package/dist/gesture-backend.d.ts +26 -0
- package/dist/gesture-backend.js +14 -0
- package/dist/gesture-backend.js.map +1 -0
- package/dist/gesture-dispatch.d.ts +67 -0
- package/dist/gesture-dispatch.js +106 -0
- package/dist/gesture-dispatch.js.map +1 -0
- package/dist/gestures.d.ts +23 -0
- package/dist/gestures.js +164 -0
- package/dist/gestures.js.map +1 -0
- package/dist/height-index.d.ts +31 -0
- package/dist/height-index.js +80 -0
- package/dist/height-index.js.map +1 -0
- package/dist/image-background.d.ts +32 -0
- package/dist/image-background.js +80 -0
- package/dist/image-background.js.map +1 -0
- package/dist/image.d.ts +79 -0
- package/dist/image.js +155 -0
- package/dist/image.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/input-accessory-view.d.ts +16 -0
- package/dist/input-accessory-view.js +29 -0
- package/dist/input-accessory-view.js.map +1 -0
- package/dist/keyboard-avoiding-view.d.ts +79 -0
- package/dist/keyboard-avoiding-view.js +172 -0
- package/dist/keyboard-avoiding-view.js.map +1 -0
- package/dist/keyboard-dock.d.ts +126 -0
- package/dist/keyboard-dock.js +327 -0
- package/dist/keyboard-dock.js.map +1 -0
- package/dist/keyboard-taps.d.ts +12 -0
- package/dist/keyboard-taps.js +51 -0
- package/dist/keyboard-taps.js.map +1 -0
- package/dist/modal.d.ts +56 -0
- package/dist/modal.js +102 -0
- package/dist/modal.js.map +1 -0
- package/dist/native-gesture.d.ts +21 -0
- package/dist/native-gesture.js +56 -0
- package/dist/native-gesture.js.map +1 -0
- package/dist/native-ref.d.ts +7 -0
- package/dist/native-ref.js +13 -0
- package/dist/native-ref.js.map +1 -0
- package/dist/pressable.d.ts +220 -0
- package/dist/pressable.js +484 -0
- package/dist/pressable.js.map +1 -0
- package/dist/reanimated.d.ts +19 -0
- package/dist/reanimated.js +111 -0
- package/dist/reanimated.js.map +1 -0
- package/dist/refresh-control.d.ts +50 -0
- package/dist/refresh-control.js +103 -0
- package/dist/refresh-control.js.map +1 -0
- package/dist/safe-area-provider.d.ts +11 -0
- package/dist/safe-area-provider.js +70 -0
- package/dist/safe-area-provider.js.map +1 -0
- package/dist/safe-area-view.d.ts +22 -0
- package/dist/safe-area-view.js +77 -0
- package/dist/safe-area-view.js.map +1 -0
- package/dist/safe-area.d.ts +12 -0
- package/dist/safe-area.js +27 -0
- package/dist/safe-area.js.map +1 -0
- package/dist/scroll-view-props.d.ts +89 -0
- package/dist/scroll-view-props.js +153 -0
- package/dist/scroll-view-props.js.map +1 -0
- package/dist/scroll-view.d.ts +95 -0
- package/dist/scroll-view.js +181 -0
- package/dist/scroll-view.js.map +1 -0
- package/dist/section-list.d.ts +146 -0
- package/dist/section-list.js +300 -0
- package/dist/section-list.js.map +1 -0
- package/dist/sticky-headers.d.ts +41 -0
- package/dist/sticky-headers.js +118 -0
- package/dist/sticky-headers.js.map +1 -0
- package/dist/swipe-refresh-layout.d.ts +51 -0
- package/dist/swipe-refresh-layout.js +162 -0
- package/dist/swipe-refresh-layout.js.map +1 -0
- package/dist/switch.d.ts +54 -0
- package/dist/switch.js +110 -0
- package/dist/switch.js.map +1 -0
- package/dist/template-slot.d.ts +18 -0
- package/dist/template-slot.js +30 -0
- package/dist/template-slot.js.map +1 -0
- package/dist/text-input.d.ts +168 -0
- package/dist/text-input.js +312 -0
- package/dist/text-input.js.map +1 -0
- package/dist/text.d.ts +70 -0
- package/dist/text.js +111 -0
- package/dist/text.js.map +1 -0
- package/dist/touchable-opacity.d.ts +36 -0
- package/dist/touchable-opacity.js +76 -0
- package/dist/touchable-opacity.js.map +1 -0
- package/dist/transforms.d.ts +2 -0
- package/dist/transforms.js +17 -0
- package/dist/transforms.js.map +1 -0
- package/dist/view-base.d.ts +332 -0
- package/dist/view-base.js +534 -0
- package/dist/view-base.js.map +1 -0
- package/dist/view.d.ts +12 -0
- package/dist/view.js +18 -0
- package/dist/view.js.map +1 -0
- package/dist/virtual-list.d.ts +530 -0
- package/dist/virtual-list.js +1150 -0
- package/dist/virtual-list.js.map +1 -0
- package/dist/worklet-scroll.d.ts +13 -0
- package/dist/worklet-scroll.js +49 -0
- package/dist/worklet-scroll.js.map +1 -0
- package/dist/worklet-style.d.ts +13 -0
- package/dist/worklet-style.js +47 -0
- package/dist/worklet-style.js.map +1 -0
- package/dist/worklets.d.ts +89 -0
- package/dist/worklets.js +42 -0
- package/dist/worklets.js.map +1 -0
- package/dist/written-props.d.ts +6 -0
- package/dist/written-props.js +180 -0
- package/dist/written-props.js.map +1 -0
- package/package.json +88 -3
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { pinnedRange } from '@ng-native/fabric';
|
|
2
|
+
/**
|
|
3
|
+
* `stickyHeaderIndices` for `<scroll-view>`, done the way RN's `ScrollView.js` does it.
|
|
4
|
+
*
|
|
5
|
+
* There is no native prop for it on either platform. RN wraps each sticky child in
|
|
6
|
+
* `ScrollViewStickyHeader`, which translates it by the scroll offset past its own position and
|
|
7
|
+
* stops where the next sticky child would push it off. The same sums run here, over the content
|
|
8
|
+
* view's element children: control-flow anchors are not children in RN's sense and are skipped,
|
|
9
|
+
* so an index counts what is committed.
|
|
10
|
+
*
|
|
11
|
+
* Each sticky child's position comes from its own layout event, and the translation is written
|
|
12
|
+
* as its `transform` prop, with RN's `zIndex: 10` so it draws over the content scrolling under
|
|
13
|
+
* it. A style of the caller's own that sets either wins, as it would in RN.
|
|
14
|
+
*
|
|
15
|
+
* Where the host can, native moves each one on every frame the scroll view moves, as RN's
|
|
16
|
+
* animated interpolation does, and the translation is written only once scrolling pauses: a
|
|
17
|
+
* translate written from each scroll event lands late, and on a fast fling the header shudders.
|
|
18
|
+
*/
|
|
19
|
+
export class StickyHeaders {
|
|
20
|
+
engine;
|
|
21
|
+
horizontal;
|
|
22
|
+
headers = [];
|
|
23
|
+
extents = new Map();
|
|
24
|
+
listening = new Map();
|
|
25
|
+
/** The translation last written per header, so an unchanged one is not written again. */
|
|
26
|
+
written = new Map();
|
|
27
|
+
drives = new Map();
|
|
28
|
+
settleTimer;
|
|
29
|
+
offset = 0;
|
|
30
|
+
constructor(engine, scroll, horizontal) {
|
|
31
|
+
this.engine = engine;
|
|
32
|
+
this.scroll = scroll;
|
|
33
|
+
this.horizontal = horizontal;
|
|
34
|
+
}
|
|
35
|
+
scroll;
|
|
36
|
+
/** Find the sticky children again, after a pass that may have changed them. */
|
|
37
|
+
track(content, indices) {
|
|
38
|
+
const children = (content?.children ?? []).filter((child) => child.kind === 'element');
|
|
39
|
+
const headers = indices.flatMap((index) => children[index] ?? []);
|
|
40
|
+
for (const node of this.headers) {
|
|
41
|
+
if (!headers.includes(node))
|
|
42
|
+
this.release(node);
|
|
43
|
+
}
|
|
44
|
+
for (const node of headers) {
|
|
45
|
+
if (this.listening.has(node))
|
|
46
|
+
continue;
|
|
47
|
+
this.engine.setProp(node, 'zIndex', 10);
|
|
48
|
+
this.listening.set(node, this.engine.setEventListener(node, 'topLayout', (event) => this.measured(node, event)));
|
|
49
|
+
}
|
|
50
|
+
this.headers = headers;
|
|
51
|
+
this.apply();
|
|
52
|
+
}
|
|
53
|
+
scrolled(offset) {
|
|
54
|
+
this.offset = offset;
|
|
55
|
+
if (!this.engine.drivesScroll)
|
|
56
|
+
return this.apply();
|
|
57
|
+
// RN's sticky header commits its translate after 64 ms without a scroll event.
|
|
58
|
+
clearTimeout(this.settleTimer);
|
|
59
|
+
this.settleTimer = setTimeout(() => this.apply(), 64);
|
|
60
|
+
}
|
|
61
|
+
destroy() {
|
|
62
|
+
clearTimeout(this.settleTimer);
|
|
63
|
+
for (const node of this.headers)
|
|
64
|
+
this.release(node);
|
|
65
|
+
this.headers = [];
|
|
66
|
+
}
|
|
67
|
+
measured(node, event) {
|
|
68
|
+
const layout = event.nativeEvent?.layout;
|
|
69
|
+
if (!layout)
|
|
70
|
+
return;
|
|
71
|
+
const horizontal = this.horizontal();
|
|
72
|
+
this.extents.set(node, {
|
|
73
|
+
start: (horizontal ? layout.x : layout.y) ?? 0,
|
|
74
|
+
size: (horizontal ? layout.width : layout.height) ?? 0,
|
|
75
|
+
});
|
|
76
|
+
this.apply();
|
|
77
|
+
}
|
|
78
|
+
release(node) {
|
|
79
|
+
this.listening.get(node)?.();
|
|
80
|
+
this.listening.delete(node);
|
|
81
|
+
this.extents.delete(node);
|
|
82
|
+
this.written.delete(node);
|
|
83
|
+
this.drives.get(node)?.stop();
|
|
84
|
+
this.drives.delete(node);
|
|
85
|
+
this.engine.setProp(node, 'zIndex', undefined);
|
|
86
|
+
this.engine.setProp(node, 'transform', undefined);
|
|
87
|
+
}
|
|
88
|
+
drive(node, start, stop) {
|
|
89
|
+
if (!this.engine.drivesScroll)
|
|
90
|
+
return;
|
|
91
|
+
const range = pinnedRange(start, stop);
|
|
92
|
+
const drive = this.drives.get(node);
|
|
93
|
+
if (drive)
|
|
94
|
+
return drive.update(range);
|
|
95
|
+
const axis = this.horizontal() ? 'x' : 'y';
|
|
96
|
+
const next = this.engine.driveByScroll(node, this.scroll, axis, range);
|
|
97
|
+
if (next)
|
|
98
|
+
this.drives.set(node, next);
|
|
99
|
+
}
|
|
100
|
+
/** `ScrollViewStickyHeader`'s interpolation, as plain arithmetic. */
|
|
101
|
+
apply() {
|
|
102
|
+
const key = this.horizontal() ? 'translateX' : 'translateY';
|
|
103
|
+
this.headers.forEach((node, at) => {
|
|
104
|
+
const own = this.extents.get(node);
|
|
105
|
+
if (!own)
|
|
106
|
+
return;
|
|
107
|
+
const next = this.extents.get(this.headers[at + 1]);
|
|
108
|
+
const stop = next ? next.start - own.size : Infinity;
|
|
109
|
+
this.drive(node, own.start, stop);
|
|
110
|
+
const shift = Math.min(Math.max(0, this.offset - own.start), Math.max(0, stop - own.start));
|
|
111
|
+
if (this.written.get(node) === shift)
|
|
112
|
+
return;
|
|
113
|
+
this.written.set(node, shift);
|
|
114
|
+
this.engine.setProp(node, 'transform', shift > 0 ? [{ [key]: shift }] : undefined);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=sticky-headers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sticky-headers.js","sourceRoot":"","sources":["../src/sticky-headers.ts"],"names":[],"mappings":"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","sourcesContent":["import { type HostEngine, type HostNode, type ScrollDrive, pinnedRange } from '@ng-native/fabric';\n\ninterface Extent {\n readonly start: number;\n readonly size: number;\n}\n\ntype LayoutEvent = {\n nativeEvent?: { layout?: { x?: number; y?: number; width?: number; height?: number } };\n};\n\n/**\n * `stickyHeaderIndices` for `<scroll-view>`, done the way RN's `ScrollView.js` does it.\n *\n * There is no native prop for it on either platform. RN wraps each sticky child in\n * `ScrollViewStickyHeader`, which translates it by the scroll offset past its own position and\n * stops where the next sticky child would push it off. The same sums run here, over the content\n * view's element children: control-flow anchors are not children in RN's sense and are skipped,\n * so an index counts what is committed.\n *\n * Each sticky child's position comes from its own layout event, and the translation is written\n * as its `transform` prop, with RN's `zIndex: 10` so it draws over the content scrolling under\n * it. A style of the caller's own that sets either wins, as it would in RN.\n *\n * Where the host can, native moves each one on every frame the scroll view moves, as RN's\n * animated interpolation does, and the translation is written only once scrolling pauses: a\n * translate written from each scroll event lands late, and on a fast fling the header shudders.\n */\nexport class StickyHeaders {\n private readonly engine: HostEngine;\n private readonly horizontal: () => boolean;\n private headers: HostNode[] = [];\n private readonly extents = new Map<HostNode, Extent>();\n private readonly listening = new Map<HostNode, () => void>();\n /** The translation last written per header, so an unchanged one is not written again. */\n private readonly written = new Map<HostNode, number>();\n private readonly drives = new Map<HostNode, ScrollDrive>();\n private settleTimer: ReturnType<typeof setTimeout> | undefined;\n private offset = 0;\n\n constructor(engine: HostEngine, scroll: HostNode, horizontal: () => boolean) {\n this.engine = engine;\n this.scroll = scroll;\n this.horizontal = horizontal;\n }\n\n private readonly scroll: HostNode;\n\n /** Find the sticky children again, after a pass that may have changed them. */\n track(content: HostNode | undefined, indices: readonly number[]): void {\n const children = (content?.children ?? []).filter((child) => child.kind === 'element');\n const headers = indices.flatMap((index) => children[index] ?? []);\n for (const node of this.headers) {\n if (!headers.includes(node)) this.release(node);\n }\n for (const node of headers) {\n if (this.listening.has(node)) continue;\n this.engine.setProp(node, 'zIndex', 10);\n this.listening.set(\n node,\n this.engine.setEventListener(node, 'topLayout', (event) => this.measured(node, event)),\n );\n }\n this.headers = headers;\n this.apply();\n }\n\n scrolled(offset: number): void {\n this.offset = offset;\n if (!this.engine.drivesScroll) return this.apply();\n // RN's sticky header commits its translate after 64 ms without a scroll event.\n clearTimeout(this.settleTimer);\n this.settleTimer = setTimeout(() => this.apply(), 64);\n }\n\n destroy(): void {\n clearTimeout(this.settleTimer);\n for (const node of this.headers) this.release(node);\n this.headers = [];\n }\n\n private measured(node: HostNode, event: unknown): void {\n const layout = (event as LayoutEvent).nativeEvent?.layout;\n if (!layout) return;\n const horizontal = this.horizontal();\n this.extents.set(node, {\n start: (horizontal ? layout.x : layout.y) ?? 0,\n size: (horizontal ? layout.width : layout.height) ?? 0,\n });\n this.apply();\n }\n\n private release(node: HostNode): void {\n this.listening.get(node)?.();\n this.listening.delete(node);\n this.extents.delete(node);\n this.written.delete(node);\n this.drives.get(node)?.stop();\n this.drives.delete(node);\n this.engine.setProp(node, 'zIndex', undefined);\n this.engine.setProp(node, 'transform', undefined);\n }\n\n private drive(node: HostNode, start: number, stop: number): void {\n if (!this.engine.drivesScroll) return;\n const range = pinnedRange(start, stop);\n const drive = this.drives.get(node);\n if (drive) return drive.update(range);\n const axis = this.horizontal() ? 'x' : 'y';\n const next = this.engine.driveByScroll(node, this.scroll, axis, range);\n if (next) this.drives.set(node, next);\n }\n\n /** `ScrollViewStickyHeader`'s interpolation, as plain arithmetic. */\n private apply(): void {\n const key = this.horizontal() ? 'translateX' : 'translateY';\n this.headers.forEach((node, at) => {\n const own = this.extents.get(node);\n if (!own) return;\n const next = this.extents.get(this.headers[at + 1]!);\n const stop = next ? next.start - own.size : Infinity;\n this.drive(node, own.start, stop);\n const shift = Math.min(Math.max(0, this.offset - own.start), Math.max(0, stop - own.start));\n if (this.written.get(node) === shift) return;\n this.written.set(node, shift);\n this.engine.setProp(node, 'transform', shift > 0 ? [{ [key]: shift }] : undefined);\n });\n }\n}\n"]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Engine, EngineNode } from '@ng-native/fabric';
|
|
2
|
+
/**
|
|
3
|
+
* The Android half of `<refresh-control>`: make its swipe layout the scroll view's parent.
|
|
4
|
+
*
|
|
5
|
+
* `ScrollView.js` does it with `cloneElement(refreshControl, {style: outer}, <NativeScrollView
|
|
6
|
+
* style={inner}>)`. `AndroidSwipeRefreshLayout` is a `ViewGroup` that pulls on its one child;
|
|
7
|
+
* committed inside the scroll view instead, it has no child to pull on and a pull does nothing.
|
|
8
|
+
*
|
|
9
|
+
* The templates cannot say it, because the refresh control is content of the scroll view, so the
|
|
10
|
+
* engine nodes are moved instead: the swipe layout goes where the scroll view was, the scroll view
|
|
11
|
+
* goes inside it, and an anchor, which never reaches Fabric, marks the spot to put it back.
|
|
12
|
+
*
|
|
13
|
+
* `sync` is idempotent and cheap when nothing moved, so it runs after every pass. That is also
|
|
14
|
+
* what repairs the shape when Angular moves the scroll view itself, say in a reordered `@for`:
|
|
15
|
+
* Angular re-inserts the node it knows about, and the next pass wraps it again where it landed.
|
|
16
|
+
*
|
|
17
|
+
* The style is split the same way RN splits it. The inline style Angular writes to the scroll view
|
|
18
|
+
* is read, the layout keys go to the swipe layout over the scroll view's base style, and the
|
|
19
|
+
* scroll view keeps the rest. A moved key is left on the scroll view as `null`, or as its base
|
|
20
|
+
* value for `flexGrow` and `flexShrink`, rather than deleted: Angular only removes a style key
|
|
21
|
+
* that is still there, so a deleted one could never be taken off the swipe layout again.
|
|
22
|
+
*
|
|
23
|
+
* ponytail: only inline style is split. Layout that reaches the scroll view from a class stays on
|
|
24
|
+
* the scroll view inside the swipe layout, which fills it anyway.
|
|
25
|
+
*/
|
|
26
|
+
export declare class SwipeRefreshLayout {
|
|
27
|
+
private readonly engine;
|
|
28
|
+
private readonly layout;
|
|
29
|
+
private scroll;
|
|
30
|
+
private readonly anchor;
|
|
31
|
+
/** The style object last written to the scroll view, to tell Angular's writes from ours. */
|
|
32
|
+
private written;
|
|
33
|
+
/** What was written in place of each moved key. */
|
|
34
|
+
private placeholders;
|
|
35
|
+
/** The caller's own values for the moved keys. */
|
|
36
|
+
private outer;
|
|
37
|
+
/** Whether `nestedScrollEnabled` was ours to set, so that unwrapping can take it back. */
|
|
38
|
+
private nested;
|
|
39
|
+
constructor(engine: Engine, layout: EngineNode);
|
|
40
|
+
sync(): void;
|
|
41
|
+
/** Put the scroll view back where the swipe layout is, with its whole style. */
|
|
42
|
+
unwrap(): void;
|
|
43
|
+
/**
|
|
44
|
+
* The scroll view this control is content of: the nearest ancestor that is one. Looked up once,
|
|
45
|
+
* before the first wrap moves the control out from under it.
|
|
46
|
+
*/
|
|
47
|
+
private findScroll;
|
|
48
|
+
/** The style as the caller wrote it: the placeholders swapped back for the values they hide. */
|
|
49
|
+
private callerStyle;
|
|
50
|
+
private split;
|
|
51
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RN's `splitLayoutProps`: the style keys that size and place a view, rather than paint it. On
|
|
3
|
+
* Android these move from the scroll view to the swipe layout that wraps it.
|
|
4
|
+
*/
|
|
5
|
+
const OUTER = new Set([
|
|
6
|
+
'margin',
|
|
7
|
+
'marginHorizontal',
|
|
8
|
+
'marginVertical',
|
|
9
|
+
'marginBottom',
|
|
10
|
+
'marginTop',
|
|
11
|
+
'marginLeft',
|
|
12
|
+
'marginRight',
|
|
13
|
+
'flex',
|
|
14
|
+
'flexGrow',
|
|
15
|
+
'flexShrink',
|
|
16
|
+
'flexBasis',
|
|
17
|
+
'alignSelf',
|
|
18
|
+
'height',
|
|
19
|
+
'minHeight',
|
|
20
|
+
'maxHeight',
|
|
21
|
+
'width',
|
|
22
|
+
'minWidth',
|
|
23
|
+
'maxWidth',
|
|
24
|
+
'position',
|
|
25
|
+
'left',
|
|
26
|
+
'right',
|
|
27
|
+
'bottom',
|
|
28
|
+
'top',
|
|
29
|
+
'transform',
|
|
30
|
+
'transformOrigin',
|
|
31
|
+
'rowGap',
|
|
32
|
+
'columnGap',
|
|
33
|
+
'gap',
|
|
34
|
+
]);
|
|
35
|
+
/** The element names whose host is a native scroll view a refresh control can belong to. */
|
|
36
|
+
const SCROLL_HOSTS = new Set(['scroll-view', 'virtual-list']);
|
|
37
|
+
/**
|
|
38
|
+
* The Android half of `<refresh-control>`: make its swipe layout the scroll view's parent.
|
|
39
|
+
*
|
|
40
|
+
* `ScrollView.js` does it with `cloneElement(refreshControl, {style: outer}, <NativeScrollView
|
|
41
|
+
* style={inner}>)`. `AndroidSwipeRefreshLayout` is a `ViewGroup` that pulls on its one child;
|
|
42
|
+
* committed inside the scroll view instead, it has no child to pull on and a pull does nothing.
|
|
43
|
+
*
|
|
44
|
+
* The templates cannot say it, because the refresh control is content of the scroll view, so the
|
|
45
|
+
* engine nodes are moved instead: the swipe layout goes where the scroll view was, the scroll view
|
|
46
|
+
* goes inside it, and an anchor, which never reaches Fabric, marks the spot to put it back.
|
|
47
|
+
*
|
|
48
|
+
* `sync` is idempotent and cheap when nothing moved, so it runs after every pass. That is also
|
|
49
|
+
* what repairs the shape when Angular moves the scroll view itself, say in a reordered `@for`:
|
|
50
|
+
* Angular re-inserts the node it knows about, and the next pass wraps it again where it landed.
|
|
51
|
+
*
|
|
52
|
+
* The style is split the same way RN splits it. The inline style Angular writes to the scroll view
|
|
53
|
+
* is read, the layout keys go to the swipe layout over the scroll view's base style, and the
|
|
54
|
+
* scroll view keeps the rest. A moved key is left on the scroll view as `null`, or as its base
|
|
55
|
+
* value for `flexGrow` and `flexShrink`, rather than deleted: Angular only removes a style key
|
|
56
|
+
* that is still there, so a deleted one could never be taken off the swipe layout again.
|
|
57
|
+
*
|
|
58
|
+
* ponytail: only inline style is split. Layout that reaches the scroll view from a class stays on
|
|
59
|
+
* the scroll view inside the swipe layout, which fills it anyway.
|
|
60
|
+
*/
|
|
61
|
+
export class SwipeRefreshLayout {
|
|
62
|
+
engine;
|
|
63
|
+
layout;
|
|
64
|
+
scroll = null;
|
|
65
|
+
anchor;
|
|
66
|
+
/** The style object last written to the scroll view, to tell Angular's writes from ours. */
|
|
67
|
+
written;
|
|
68
|
+
/** What was written in place of each moved key. */
|
|
69
|
+
placeholders = {};
|
|
70
|
+
/** The caller's own values for the moved keys. */
|
|
71
|
+
outer = {};
|
|
72
|
+
/** Whether `nestedScrollEnabled` was ours to set, so that unwrapping can take it back. */
|
|
73
|
+
nested = false;
|
|
74
|
+
constructor(engine, layout) {
|
|
75
|
+
this.engine = engine;
|
|
76
|
+
this.layout = layout;
|
|
77
|
+
this.anchor = engine.createAnchor();
|
|
78
|
+
}
|
|
79
|
+
sync() {
|
|
80
|
+
const scroll = (this.scroll ??= this.findScroll());
|
|
81
|
+
const parent = scroll?.parent;
|
|
82
|
+
if (!scroll || !parent)
|
|
83
|
+
return;
|
|
84
|
+
if (parent !== this.layout) {
|
|
85
|
+
this.engine.insertBefore(parent, this.anchor, scroll);
|
|
86
|
+
this.engine.insertBefore(parent, this.layout, scroll);
|
|
87
|
+
this.engine.appendChild(this.layout, scroll);
|
|
88
|
+
}
|
|
89
|
+
// RN's `nestedScrollEnabled ?? true`. `<scroll-view>` and `<virtual-list>` bind it themselves
|
|
90
|
+
// when they can see the control; this covers one they cannot, projected through another
|
|
91
|
+
// component such as `<section-list>`.
|
|
92
|
+
if (scroll.props['nestedScrollEnabled'] === undefined) {
|
|
93
|
+
this.engine.setProp(scroll, 'nestedScrollEnabled', true);
|
|
94
|
+
this.nested = true;
|
|
95
|
+
}
|
|
96
|
+
const style = scroll.props['style'];
|
|
97
|
+
if (style !== this.written || this.layout.props['style'] === undefined)
|
|
98
|
+
this.split(scroll);
|
|
99
|
+
}
|
|
100
|
+
/** Put the scroll view back where the swipe layout is, with its whole style. */
|
|
101
|
+
unwrap() {
|
|
102
|
+
const scroll = this.scroll;
|
|
103
|
+
if (scroll) {
|
|
104
|
+
const home = this.anchor.parent;
|
|
105
|
+
if (scroll.parent === this.layout && home)
|
|
106
|
+
this.engine.insertBefore(home, scroll, this.anchor);
|
|
107
|
+
const style = scroll.props['style'];
|
|
108
|
+
if (style === this.written)
|
|
109
|
+
this.engine.setProp(scroll, 'style', this.callerStyle(style));
|
|
110
|
+
if (this.nested)
|
|
111
|
+
this.engine.setProp(scroll, 'nestedScrollEnabled', undefined);
|
|
112
|
+
}
|
|
113
|
+
this.engine.removeChild(null, this.layout);
|
|
114
|
+
this.engine.removeChild(null, this.anchor);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The scroll view this control is content of: the nearest ancestor that is one. Looked up once,
|
|
118
|
+
* before the first wrap moves the control out from under it.
|
|
119
|
+
*/
|
|
120
|
+
findScroll() {
|
|
121
|
+
for (let node = this.layout.parent; node; node = node.parent) {
|
|
122
|
+
if (SCROLL_HOSTS.has(node.name))
|
|
123
|
+
return node;
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
/** The style as the caller wrote it: the placeholders swapped back for the values they hide. */
|
|
128
|
+
callerStyle(style) {
|
|
129
|
+
const caller = {};
|
|
130
|
+
for (const [key, value] of Object.entries(style ?? {})) {
|
|
131
|
+
const hidden = key in this.placeholders && value === this.placeholders[key];
|
|
132
|
+
caller[key] = hidden ? this.outer[key] : value;
|
|
133
|
+
}
|
|
134
|
+
return caller;
|
|
135
|
+
}
|
|
136
|
+
split(scroll) {
|
|
137
|
+
const outer = {};
|
|
138
|
+
const inner = {};
|
|
139
|
+
const placeholders = {};
|
|
140
|
+
for (const [key, value] of Object.entries(this.callerStyle(scroll.props['style']))) {
|
|
141
|
+
if (!OUTER.has(key)) {
|
|
142
|
+
inner[key] = value;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
outer[key] = value;
|
|
146
|
+
placeholders[key] = key === 'flexGrow' || key === 'flexShrink' ? 1 : null;
|
|
147
|
+
}
|
|
148
|
+
this.outer = outer;
|
|
149
|
+
this.placeholders = placeholders;
|
|
150
|
+
this.written = { ...inner, ...placeholders };
|
|
151
|
+
this.engine.setProp(scroll, 'style', this.written);
|
|
152
|
+
// RN's `compose(baseStyle, outer)`; the direction is the one the scroll view was given.
|
|
153
|
+
this.engine.setProp(this.layout, 'style', {
|
|
154
|
+
flexGrow: 1,
|
|
155
|
+
flexShrink: 1,
|
|
156
|
+
flexDirection: scroll.props['flexDirection'] ?? 'column',
|
|
157
|
+
overflow: 'scroll',
|
|
158
|
+
...outer,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=swipe-refresh-layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"swipe-refresh-layout.js","sourceRoot":"","sources":["../src/swipe-refresh-layout.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Engine, EngineNode } from '@ng-native/fabric';\n\n/**\n * RN's `splitLayoutProps`: the style keys that size and place a view, rather than paint it. On\n * Android these move from the scroll view to the swipe layout that wraps it.\n */\nconst OUTER = new Set([\n 'margin',\n 'marginHorizontal',\n 'marginVertical',\n 'marginBottom',\n 'marginTop',\n 'marginLeft',\n 'marginRight',\n 'flex',\n 'flexGrow',\n 'flexShrink',\n 'flexBasis',\n 'alignSelf',\n 'height',\n 'minHeight',\n 'maxHeight',\n 'width',\n 'minWidth',\n 'maxWidth',\n 'position',\n 'left',\n 'right',\n 'bottom',\n 'top',\n 'transform',\n 'transformOrigin',\n 'rowGap',\n 'columnGap',\n 'gap',\n]);\n\n/** The element names whose host is a native scroll view a refresh control can belong to. */\nconst SCROLL_HOSTS = new Set(['scroll-view', 'virtual-list']);\n\ntype Style = Record<string, unknown>;\n\n/**\n * The Android half of `<refresh-control>`: make its swipe layout the scroll view's parent.\n *\n * `ScrollView.js` does it with `cloneElement(refreshControl, {style: outer}, <NativeScrollView\n * style={inner}>)`. `AndroidSwipeRefreshLayout` is a `ViewGroup` that pulls on its one child;\n * committed inside the scroll view instead, it has no child to pull on and a pull does nothing.\n *\n * The templates cannot say it, because the refresh control is content of the scroll view, so the\n * engine nodes are moved instead: the swipe layout goes where the scroll view was, the scroll view\n * goes inside it, and an anchor, which never reaches Fabric, marks the spot to put it back.\n *\n * `sync` is idempotent and cheap when nothing moved, so it runs after every pass. That is also\n * what repairs the shape when Angular moves the scroll view itself, say in a reordered `@for`:\n * Angular re-inserts the node it knows about, and the next pass wraps it again where it landed.\n *\n * The style is split the same way RN splits it. The inline style Angular writes to the scroll view\n * is read, the layout keys go to the swipe layout over the scroll view's base style, and the\n * scroll view keeps the rest. A moved key is left on the scroll view as `null`, or as its base\n * value for `flexGrow` and `flexShrink`, rather than deleted: Angular only removes a style key\n * that is still there, so a deleted one could never be taken off the swipe layout again.\n *\n * ponytail: only inline style is split. Layout that reaches the scroll view from a class stays on\n * the scroll view inside the swipe layout, which fills it anyway.\n */\nexport class SwipeRefreshLayout {\n private readonly engine: Engine;\n private readonly layout: EngineNode;\n private scroll: EngineNode | null = null;\n private readonly anchor: EngineNode;\n /** The style object last written to the scroll view, to tell Angular's writes from ours. */\n private written: Style | undefined;\n /** What was written in place of each moved key. */\n private placeholders: Style = {};\n /** The caller's own values for the moved keys. */\n private outer: Style = {};\n /** Whether `nestedScrollEnabled` was ours to set, so that unwrapping can take it back. */\n private nested = false;\n\n constructor(engine: Engine, layout: EngineNode) {\n this.engine = engine;\n this.layout = layout;\n this.anchor = engine.createAnchor();\n }\n\n sync(): void {\n const scroll = (this.scroll ??= this.findScroll());\n const parent = scroll?.parent;\n if (!scroll || !parent) return;\n\n if (parent !== this.layout) {\n this.engine.insertBefore(parent, this.anchor, scroll);\n this.engine.insertBefore(parent, this.layout, scroll);\n this.engine.appendChild(this.layout, scroll);\n }\n // RN's `nestedScrollEnabled ?? true`. `<scroll-view>` and `<virtual-list>` bind it themselves\n // when they can see the control; this covers one they cannot, projected through another\n // component such as `<section-list>`.\n if (scroll.props['nestedScrollEnabled'] === undefined) {\n this.engine.setProp(scroll, 'nestedScrollEnabled', true);\n this.nested = true;\n }\n const style = scroll.props['style'] as Style | undefined;\n if (style !== this.written || this.layout.props['style'] === undefined) this.split(scroll);\n }\n\n /** Put the scroll view back where the swipe layout is, with its whole style. */\n unwrap(): void {\n const scroll = this.scroll;\n if (scroll) {\n const home = this.anchor.parent;\n if (scroll.parent === this.layout && home)\n this.engine.insertBefore(home, scroll, this.anchor);\n const style = scroll.props['style'] as Style | undefined;\n if (style === this.written) this.engine.setProp(scroll, 'style', this.callerStyle(style));\n if (this.nested) this.engine.setProp(scroll, 'nestedScrollEnabled', undefined);\n }\n this.engine.removeChild(null, this.layout);\n this.engine.removeChild(null, this.anchor);\n }\n\n /**\n * The scroll view this control is content of: the nearest ancestor that is one. Looked up once,\n * before the first wrap moves the control out from under it.\n */\n private findScroll(): EngineNode | null {\n for (let node = this.layout.parent; node; node = node.parent) {\n if (SCROLL_HOSTS.has(node.name)) return node;\n }\n return null;\n }\n\n /** The style as the caller wrote it: the placeholders swapped back for the values they hide. */\n private callerStyle(style: Style | undefined): Style {\n const caller: Style = {};\n for (const [key, value] of Object.entries(style ?? {})) {\n const hidden = key in this.placeholders && value === this.placeholders[key];\n caller[key] = hidden ? this.outer[key] : value;\n }\n return caller;\n }\n\n private split(scroll: EngineNode): void {\n const outer: Style = {};\n const inner: Style = {};\n const placeholders: Style = {};\n for (const [key, value] of Object.entries(this.callerStyle(scroll.props['style'] as Style))) {\n if (!OUTER.has(key)) {\n inner[key] = value;\n continue;\n }\n outer[key] = value;\n placeholders[key] = key === 'flexGrow' || key === 'flexShrink' ? 1 : null;\n }\n this.outer = outer;\n this.placeholders = placeholders;\n this.written = { ...inner, ...placeholders };\n this.engine.setProp(scroll, 'style', this.written);\n // RN's `compose(baseStyle, outer)`; the direction is the one the scroll view was given.\n this.engine.setProp(this.layout, 'style', {\n flexGrow: 1,\n flexShrink: 1,\n flexDirection: scroll.props['flexDirection'] ?? 'column',\n overflow: 'scroll',\n ...outer,\n });\n }\n}\n"]}
|
package/dist/switch.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type SimpleChanges } from '@angular/core';
|
|
2
|
+
import { type NativeSyntheticEvent } from '@ng-native/fabric';
|
|
3
|
+
import { ViewBase } from './view-base.ts';
|
|
4
|
+
import * as i0 from "@angular/core";
|
|
5
|
+
/**
|
|
6
|
+
* A toggle, wired for signal forms as a `FormCheckboxControl` (`checked`, not `value`). Commits
|
|
7
|
+
* as `Switch` on iOS and `AndroidSwitch` on Android.
|
|
8
|
+
*
|
|
9
|
+
* The two platforms spell the same props differently, so both spellings are sent and each
|
|
10
|
+
* native side reads its own; a prop it does not know is ignored.
|
|
11
|
+
*
|
|
12
|
+
* Controlled as React Native's `Switch` is: bind `checked` and the app's value is what the switch
|
|
13
|
+
* shows. The user flips it, the app keeps the old value, and a prop diff has nothing to send,
|
|
14
|
+
* because the prop never changed; native is the only side that moved. So once the pass the flip
|
|
15
|
+
* started has rendered, the switch compares the model with what native reported and, if they
|
|
16
|
+
* differ, sends the command RN's wrapper sends - `setValue` on iOS, `setNativeValue` on Android.
|
|
17
|
+
* See `ControlledModel` for how the model comes to say what the app says.
|
|
18
|
+
*/
|
|
19
|
+
export declare class Switch extends ViewBase {
|
|
20
|
+
readonly checked: import("@angular/core").ModelSignal<boolean>;
|
|
21
|
+
/** Ignore touches and grey out. */
|
|
22
|
+
readonly disabled: import("@angular/core").InputSignalWithTransform<boolean | undefined, unknown>;
|
|
23
|
+
/** `Switch.js`: `accessibilityRole={props.accessibilityRole ?? 'switch'}`. */
|
|
24
|
+
protected roleByDefault(): 'switch';
|
|
25
|
+
protected disabledForAccessibility(): boolean | undefined;
|
|
26
|
+
protected checkedForAccessibility(): boolean;
|
|
27
|
+
/** The colour of the knob. */
|
|
28
|
+
readonly thumbColor: import("@angular/core").InputSignal<string | undefined>;
|
|
29
|
+
/** The track colour for each state. */
|
|
30
|
+
readonly trackColor: import("@angular/core").InputSignal<{
|
|
31
|
+
readonly false?: string;
|
|
32
|
+
readonly true?: string;
|
|
33
|
+
} | undefined>;
|
|
34
|
+
/** iOS: the colour behind the track when off, seen where the track is transparent. */
|
|
35
|
+
readonly iosBackgroundColor: import("@angular/core").InputSignal<string | undefined>;
|
|
36
|
+
protected readonly enabled: import("@angular/core").Signal<boolean | undefined>;
|
|
37
|
+
private readonly injector;
|
|
38
|
+
private readonly control;
|
|
39
|
+
/** What native reported last, until the pass it started has rendered and been compared. */
|
|
40
|
+
private nativeValue;
|
|
41
|
+
constructor();
|
|
42
|
+
ngOnChanges(changes: SimpleChanges): void;
|
|
43
|
+
protected onNativeChange(event: NativeSyntheticEvent<{
|
|
44
|
+
value?: boolean;
|
|
45
|
+
}>): void;
|
|
46
|
+
/**
|
|
47
|
+
* After the app's bindings have settled, not before: a flip the app accepts reaches the model
|
|
48
|
+
* during change detection, and comparing any earlier would send native back the value it is
|
|
49
|
+
* about to be given. A value set from code later needs none of this, as the prop changes.
|
|
50
|
+
*/
|
|
51
|
+
private reconcile;
|
|
52
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<Switch, never>;
|
|
53
|
+
static ɵdir: i0.ɵɵDirectiveDeclaration<Switch, "switch", never, { "checked": { "alias": "checked"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "thumbColor": { "alias": "thumbColor"; "required": false; "isSignal": true; }; "trackColor": { "alias": "trackColor"; "required": false; "isSignal": true; }; "iosBackgroundColor": { "alias": "ios_backgroundColor"; "required": false; "isSignal": true; }; }, { "checked": "checkedChange"; }, never, never, true, never>;
|
|
54
|
+
}
|
package/dist/switch.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Directive, Injector, afterNextRender, computed, effect, inject, input, model, } from '@angular/core';
|
|
2
|
+
import { nativePlatform } from '@ng-native/fabric';
|
|
3
|
+
import { ControlledModel } from "./controlled-model.js";
|
|
4
|
+
import { optionalBoolean } from "./transforms.js";
|
|
5
|
+
import { ViewBase } from "./view-base.js";
|
|
6
|
+
import * as i0 from "@angular/core";
|
|
7
|
+
/**
|
|
8
|
+
* A toggle, wired for signal forms as a `FormCheckboxControl` (`checked`, not `value`). Commits
|
|
9
|
+
* as `Switch` on iOS and `AndroidSwitch` on Android.
|
|
10
|
+
*
|
|
11
|
+
* The two platforms spell the same props differently, so both spellings are sent and each
|
|
12
|
+
* native side reads its own; a prop it does not know is ignored.
|
|
13
|
+
*
|
|
14
|
+
* Controlled as React Native's `Switch` is: bind `checked` and the app's value is what the switch
|
|
15
|
+
* shows. The user flips it, the app keeps the old value, and a prop diff has nothing to send,
|
|
16
|
+
* because the prop never changed; native is the only side that moved. So once the pass the flip
|
|
17
|
+
* started has rendered, the switch compares the model with what native reported and, if they
|
|
18
|
+
* differ, sends the command RN's wrapper sends - `setValue` on iOS, `setNativeValue` on Android.
|
|
19
|
+
* See `ControlledModel` for how the model comes to say what the app says.
|
|
20
|
+
*/
|
|
21
|
+
export class Switch extends ViewBase {
|
|
22
|
+
checked = model(false, /* @ts-ignore */
|
|
23
|
+
...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
|
|
24
|
+
/** Ignore touches and grey out. */
|
|
25
|
+
disabled = input(undefined, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: optionalBoolean });
|
|
26
|
+
/** `Switch.js`: `accessibilityRole={props.accessibilityRole ?? 'switch'}`. */
|
|
27
|
+
roleByDefault() {
|
|
28
|
+
return 'switch';
|
|
29
|
+
}
|
|
30
|
+
disabledForAccessibility() {
|
|
31
|
+
return this.disabled();
|
|
32
|
+
}
|
|
33
|
+
checkedForAccessibility() {
|
|
34
|
+
return this.checked();
|
|
35
|
+
}
|
|
36
|
+
/** The colour of the knob. */
|
|
37
|
+
thumbColor = input(/* @ts-ignore */
|
|
38
|
+
...(ngDevMode ? [undefined, { debugName: "thumbColor" }] : /* istanbul ignore next */ []));
|
|
39
|
+
/** The track colour for each state. */
|
|
40
|
+
trackColor = input(/* @ts-ignore */
|
|
41
|
+
...(ngDevMode ? [undefined, { debugName: "trackColor" }] : /* istanbul ignore next */ []));
|
|
42
|
+
/** iOS: the colour behind the track when off, seen where the track is transparent. */
|
|
43
|
+
iosBackgroundColor = input(undefined, { ...(ngDevMode ? { debugName: "iosBackgroundColor" } : /* istanbul ignore next */ {}), alias: 'ios_backgroundColor' });
|
|
44
|
+
enabled = computed(() => {
|
|
45
|
+
const disabled = this.disabled();
|
|
46
|
+
return disabled === undefined ? undefined : !disabled;
|
|
47
|
+
}, /* @ts-ignore */
|
|
48
|
+
...(ngDevMode ? [{ debugName: "enabled" }] : /* istanbul ignore next */ []));
|
|
49
|
+
injector = inject(Injector);
|
|
50
|
+
control = new ControlledModel(this.checked, 'checked');
|
|
51
|
+
/** What native reported last, until the pass it started has rendered and been compared. */
|
|
52
|
+
nativeValue = null;
|
|
53
|
+
constructor() {
|
|
54
|
+
super();
|
|
55
|
+
effect(() => {
|
|
56
|
+
// Android spells the value `on` and iOS the on-track colour `onTintColor`. Angular refuses
|
|
57
|
+
// both as host bindings because they read as event handlers, so they go to the node.
|
|
58
|
+
this.engine.setProp(this.node, 'on', this.checked());
|
|
59
|
+
this.engine.setProp(this.node, 'onTintColor', this.trackColor()?.true);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
ngOnChanges(changes) {
|
|
63
|
+
super.ngOnChanges(changes);
|
|
64
|
+
this.control.noteChanges(changes);
|
|
65
|
+
}
|
|
66
|
+
onNativeChange(event) {
|
|
67
|
+
this.nativeValue = event.nativeEvent?.value ?? false;
|
|
68
|
+
this.control.propose(this.nativeValue);
|
|
69
|
+
afterNextRender({ write: () => this.reconcile() }, { injector: this.injector });
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* After the app's bindings have settled, not before: a flip the app accepts reaches the model
|
|
73
|
+
* during change detection, and comparing any earlier would send native back the value it is
|
|
74
|
+
* about to be given. A value set from code later needs none of this, as the prop changes.
|
|
75
|
+
*/
|
|
76
|
+
reconcile() {
|
|
77
|
+
const native = this.nativeValue;
|
|
78
|
+
this.nativeValue = null;
|
|
79
|
+
const desired = this.checked();
|
|
80
|
+
if (native === null || native === desired)
|
|
81
|
+
return;
|
|
82
|
+
const command = nativePlatform() === 'android' ? 'setNativeValue' : 'setValue';
|
|
83
|
+
this.engine.dispatchCommand(this.node, command, [desired]);
|
|
84
|
+
}
|
|
85
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: Switch, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
86
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.2.0", type: Switch, isStandalone: true, selector: "switch", inputs: { checked: { classPropertyName: "checked", publicName: "checked", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, thumbColor: { classPropertyName: "thumbColor", publicName: "thumbColor", isSignal: true, isRequired: false, transformFunction: null }, trackColor: { classPropertyName: "trackColor", publicName: "trackColor", isSignal: true, isRequired: false, transformFunction: null }, iosBackgroundColor: { classPropertyName: "iosBackgroundColor", publicName: "ios_backgroundColor", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { checked: "checkedChange" }, host: { listeners: { "change": "onNativeChange($event)" }, properties: { "accessibilityState": "accessibilityStateProp()", "value": "checked()", "disabled": "disabled()", "enabled": "enabled()", "thumbTintColor": "thumbColor()", "tintColor": "trackColor()?.false", "trackColorForTrue": "trackColor()?.true", "trackColorForFalse": "trackColor()?.false", "trackTintColor": "checked() ? trackColor()?.true : trackColor()?.false", "style.backgroundColor": "iosBackgroundColor()", "style.borderRadius": "iosBackgroundColor() ? 16 : undefined" } }, usesInheritance: true, usesOnChanges: true, ngImport: i0 });
|
|
87
|
+
}
|
|
88
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: Switch, decorators: [{
|
|
89
|
+
type: Directive,
|
|
90
|
+
args: [{
|
|
91
|
+
selector: 'switch',
|
|
92
|
+
host: {
|
|
93
|
+
// `disabled` is an input of ours, so it cannot reach `ngOnChanges` on the base; this is
|
|
94
|
+
// the one binding that keeps the announced state in step with it.
|
|
95
|
+
'[accessibilityState]': 'accessibilityStateProp()',
|
|
96
|
+
'[value]': 'checked()',
|
|
97
|
+
'[disabled]': 'disabled()',
|
|
98
|
+
'[enabled]': 'enabled()',
|
|
99
|
+
'[thumbTintColor]': 'thumbColor()',
|
|
100
|
+
'[tintColor]': 'trackColor()?.false',
|
|
101
|
+
'[trackColorForTrue]': 'trackColor()?.true',
|
|
102
|
+
'[trackColorForFalse]': 'trackColor()?.false',
|
|
103
|
+
'[trackTintColor]': 'checked() ? trackColor()?.true : trackColor()?.false',
|
|
104
|
+
'[style.backgroundColor]': 'iosBackgroundColor()',
|
|
105
|
+
'[style.borderRadius]': 'iosBackgroundColor() ? 16 : undefined',
|
|
106
|
+
'(change)': 'onNativeChange($event)',
|
|
107
|
+
},
|
|
108
|
+
}]
|
|
109
|
+
}], ctorParameters: () => [], propDecorators: { checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }, { type: i0.Output, args: ["checkedChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], thumbColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "thumbColor", required: false }] }], trackColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackColor", required: false }] }], iosBackgroundColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "ios_backgroundColor", required: false }] }] } });
|
|
110
|
+
//# sourceMappingURL=switch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"switch.js","sourceRoot":"","sources":["../src/switch.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Directive,\n Injector,\n type SimpleChanges,\n afterNextRender,\n computed,\n effect,\n inject,\n input,\n model,\n} from '@angular/core';\nimport { type NativeSyntheticEvent, nativePlatform } from '@ng-native/fabric';\nimport { ControlledModel } from './controlled-model.ts';\nimport { optionalBoolean } from './transforms.ts';\nimport { ViewBase } from './view-base.ts';\n\n/**\n * A toggle, wired for signal forms as a `FormCheckboxControl` (`checked`, not `value`). Commits\n * as `Switch` on iOS and `AndroidSwitch` on Android.\n *\n * The two platforms spell the same props differently, so both spellings are sent and each\n * native side reads its own; a prop it does not know is ignored.\n *\n * Controlled as React Native's `Switch` is: bind `checked` and the app's value is what the switch\n * shows. The user flips it, the app keeps the old value, and a prop diff has nothing to send,\n * because the prop never changed; native is the only side that moved. So once the pass the flip\n * started has rendered, the switch compares the model with what native reported and, if they\n * differ, sends the command RN's wrapper sends - `setValue` on iOS, `setNativeValue` on Android.\n * See `ControlledModel` for how the model comes to say what the app says.\n */\n@Directive({\n selector: 'switch',\n host: {\n // `disabled` is an input of ours, so it cannot reach `ngOnChanges` on the base; this is\n // the one binding that keeps the announced state in step with it.\n '[accessibilityState]': 'accessibilityStateProp()',\n '[value]': 'checked()',\n '[disabled]': 'disabled()',\n '[enabled]': 'enabled()',\n '[thumbTintColor]': 'thumbColor()',\n '[tintColor]': 'trackColor()?.false',\n '[trackColorForTrue]': 'trackColor()?.true',\n '[trackColorForFalse]': 'trackColor()?.false',\n '[trackTintColor]': 'checked() ? trackColor()?.true : trackColor()?.false',\n '[style.backgroundColor]': 'iosBackgroundColor()',\n '[style.borderRadius]': 'iosBackgroundColor() ? 16 : undefined',\n '(change)': 'onNativeChange($event)',\n },\n})\nexport class Switch extends ViewBase {\n readonly checked = model<boolean>(false);\n /** Ignore touches and grey out. */\n readonly disabled = input(undefined, { transform: optionalBoolean });\n\n /** `Switch.js`: `accessibilityRole={props.accessibilityRole ?? 'switch'}`. */\n protected override roleByDefault(): 'switch' {\n return 'switch';\n }\n\n protected override disabledForAccessibility(): boolean | undefined {\n return this.disabled();\n }\n\n protected override checkedForAccessibility(): boolean {\n return this.checked();\n }\n /** The colour of the knob. */\n readonly thumbColor = input<string>();\n /** The track colour for each state. */\n readonly trackColor = input<{ readonly false?: string; readonly true?: string }>();\n /** iOS: the colour behind the track when off, seen where the track is transparent. */\n readonly iosBackgroundColor = input<string>(undefined, { alias: 'ios_backgroundColor' });\n\n protected readonly enabled = computed(() => {\n const disabled = this.disabled();\n return disabled === undefined ? undefined : !disabled;\n });\n\n private readonly injector = inject(Injector);\n private readonly control = new ControlledModel(this.checked, 'checked');\n /** What native reported last, until the pass it started has rendered and been compared. */\n private nativeValue: boolean | null = null;\n\n constructor() {\n super();\n effect(() => {\n // Android spells the value `on` and iOS the on-track colour `onTintColor`. Angular refuses\n // both as host bindings because they read as event handlers, so they go to the node.\n this.engine.setProp(this.node, 'on', this.checked());\n this.engine.setProp(this.node, 'onTintColor', this.trackColor()?.true);\n });\n }\n\n override ngOnChanges(changes: SimpleChanges): void {\n super.ngOnChanges(changes);\n this.control.noteChanges(changes);\n }\n\n protected onNativeChange(event: NativeSyntheticEvent<{ value?: boolean }>): void {\n this.nativeValue = event.nativeEvent?.value ?? false;\n this.control.propose(this.nativeValue);\n afterNextRender({ write: () => this.reconcile() }, { injector: this.injector });\n }\n\n /**\n * After the app's bindings have settled, not before: a flip the app accepts reaches the model\n * during change detection, and comparing any earlier would send native back the value it is\n * about to be given. A value set from code later needs none of this, as the prop changes.\n */\n private reconcile(): void {\n const native = this.nativeValue;\n this.nativeValue = null;\n const desired = this.checked();\n if (native === null || native === desired) return;\n const command = nativePlatform() === 'android' ? 'setNativeValue' : 'setValue';\n this.engine.dispatchCommand(this.node, command, [desired]);\n }\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type OnChanges, type SimpleChanges, TemplateRef } from '@angular/core';
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
/**
|
|
4
|
+
* Stamp out a caller's `<ng-template>` with a context, which is `NgTemplateOutlet` without
|
|
5
|
+
* depending on `@angular/common` for it.
|
|
6
|
+
*
|
|
7
|
+
* The view is made once per template and its context updated in place after that, so a list that
|
|
8
|
+
* re-renders with new neighbours for a separator does not rebuild the separator.
|
|
9
|
+
*/
|
|
10
|
+
export declare class TemplateSlot<C extends object> implements OnChanges {
|
|
11
|
+
readonly template: import("@angular/core").InputSignal<TemplateRef<C>>;
|
|
12
|
+
readonly context: import("@angular/core").InputSignal<C>;
|
|
13
|
+
private readonly container;
|
|
14
|
+
private view;
|
|
15
|
+
ngOnChanges(changes: SimpleChanges): void;
|
|
16
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<TemplateSlot<any>, never>;
|
|
17
|
+
static ɵdir: i0.ɵɵDirectiveDeclaration<TemplateSlot<any>, "[templateSlot]", never, { "template": { "alias": "templateSlot"; "required": true; "isSignal": true; }; "context": { "alias": "templateSlotContext"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Directive, TemplateRef, ViewContainerRef, inject, input, } from '@angular/core';
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
/**
|
|
4
|
+
* Stamp out a caller's `<ng-template>` with a context, which is `NgTemplateOutlet` without
|
|
5
|
+
* depending on `@angular/common` for it.
|
|
6
|
+
*
|
|
7
|
+
* The view is made once per template and its context updated in place after that, so a list that
|
|
8
|
+
* re-renders with new neighbours for a separator does not rebuild the separator.
|
|
9
|
+
*/
|
|
10
|
+
export class TemplateSlot {
|
|
11
|
+
template = input.required({ ...(ngDevMode ? { debugName: "template" } : /* istanbul ignore next */ {}), alias: 'templateSlot' });
|
|
12
|
+
context = input.required({ ...(ngDevMode ? { debugName: "context" } : /* istanbul ignore next */ {}), alias: 'templateSlotContext' });
|
|
13
|
+
container = inject(ViewContainerRef);
|
|
14
|
+
view = null;
|
|
15
|
+
ngOnChanges(changes) {
|
|
16
|
+
if (this.view && !changes['template']) {
|
|
17
|
+
Object.assign(this.view.context, this.context());
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
this.container.clear();
|
|
21
|
+
this.view = this.container.createEmbeddedView(this.template(), { ...this.context() });
|
|
22
|
+
}
|
|
23
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: TemplateSlot, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
24
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.2.0", type: TemplateSlot, isStandalone: true, selector: "[templateSlot]", inputs: { template: { classPropertyName: "template", publicName: "templateSlot", isSignal: true, isRequired: true, transformFunction: null }, context: { classPropertyName: "context", publicName: "templateSlotContext", isSignal: true, isRequired: true, transformFunction: null } }, usesOnChanges: true, ngImport: i0 });
|
|
25
|
+
}
|
|
26
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: TemplateSlot, decorators: [{
|
|
27
|
+
type: Directive,
|
|
28
|
+
args: [{ selector: '[templateSlot]' }]
|
|
29
|
+
}], propDecorators: { template: [{ type: i0.Input, args: [{ isSignal: true, alias: "templateSlot", required: true }] }], context: [{ type: i0.Input, args: [{ isSignal: true, alias: "templateSlotContext", required: true }] }] } });
|
|
30
|
+
//# sourceMappingURL=template-slot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template-slot.js","sourceRoot":"","sources":["../src/template-slot.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EAIT,WAAW,EACX,gBAAgB,EAChB,MAAM,EACN,KAAK,GACN,MAAM,eAAe,CAAC;;AAEvB;;;;;;GAMG;AAEH,MAAM,OAAO,YAAY;IACd,QAAQ,GAAG,KAAK,CAAC,QAAQ,+EAAmB,KAAK,EAAE,cAAc,GAAG,CAAC;IACrE,OAAO,GAAG,KAAK,CAAC,QAAQ,8EAAM,KAAK,EAAE,qBAAqB,GAAG,CAAC;IAEtD,SAAS,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC;IAC9C,IAAI,GAA8B,IAAI,CAAC;IAE/C,WAAW,CAAC,OAAsB;QAChC,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;YACtC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;YACjD,OAAO;QACT,CAAC;QACD,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,kBAAkB,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACxF,CAAC;uGAdU,YAAY;2FAAZ,YAAY;;2FAAZ,YAAY;kBADxB,SAAS;mBAAC,EAAE,QAAQ,EAAE,gBAAgB,EAAE","sourcesContent":["import {\n Directive,\n type EmbeddedViewRef,\n type OnChanges,\n type SimpleChanges,\n TemplateRef,\n ViewContainerRef,\n inject,\n input,\n} from '@angular/core';\n\n/**\n * Stamp out a caller's `<ng-template>` with a context, which is `NgTemplateOutlet` without\n * depending on `@angular/common` for it.\n *\n * The view is made once per template and its context updated in place after that, so a list that\n * re-renders with new neighbours for a separator does not rebuild the separator.\n */\n@Directive({ selector: '[templateSlot]' })\nexport class TemplateSlot<C extends object> implements OnChanges {\n readonly template = input.required<TemplateRef<C>>({ alias: 'templateSlot' });\n readonly context = input.required<C>({ alias: 'templateSlotContext' });\n\n private readonly container = inject(ViewContainerRef);\n private view: EmbeddedViewRef<C> | null = null;\n\n ngOnChanges(changes: SimpleChanges): void {\n if (this.view && !changes['template']) {\n Object.assign(this.view.context, this.context());\n return;\n }\n this.container.clear();\n this.view = this.container.createEmbeddedView(this.template(), { ...this.context() });\n }\n}\n"]}
|