@rdlabo/ionic-theme-ios27 1.1.0 → 1.2.0-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/README.md +8 -1
- package/dist/css/components/ion-list.css +1 -1
- package/dist/css/components/ion-menu.css +1 -1
- package/dist/css/ionic-theme-ios27.css +1 -1
- package/dist/css/vertical-bars.css +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -8
- package/dist/native/components/index.d.ts +1 -0
- package/dist/native/components/index.d.ts.map +1 -1
- package/dist/native/components/index.js +8 -3
- package/dist/native/components/ion-back-button.d.ts.map +1 -1
- package/dist/native/components/ion-back-button.js +5 -2
- package/dist/native/components/ion-button.d.ts.map +1 -1
- package/dist/native/components/ion-button.js +10 -2
- package/dist/native/components/ion-buttons.d.ts.map +1 -1
- package/dist/native/components/ion-buttons.js +12 -4
- package/dist/native/components/ion-fab.js +4 -4
- package/dist/native/components/ion-menu-button.d.ts.map +1 -1
- package/dist/native/components/ion-menu-button.js +5 -4
- package/dist/native/components/searchable-tabs.d.ts.map +1 -1
- package/dist/native/components/searchable-tabs.js +4 -1
- package/dist/native/definitions.d.ts +29 -3
- package/dist/native/definitions.d.ts.map +1 -1
- package/dist/native/definitions.js +6 -1
- package/dist/native/index.d.ts +6 -2
- package/dist/native/index.d.ts.map +1 -1
- package/dist/native/index.js +154 -17
- package/dist/native/prehide.d.ts +5 -0
- package/dist/native/prehide.d.ts.map +1 -0
- package/dist/native/prehide.js +246 -0
- package/dist/native/runtime.d.ts +1 -1
- package/dist/native/runtime.d.ts.map +1 -1
- package/dist/native/runtime.js +162 -28
- package/dist/native/shared/candidate.d.ts.map +1 -1
- package/dist/native/shared/candidate.js +7 -3
- package/dist/native/shared/dom.d.ts +33 -2
- package/dist/native/shared/dom.d.ts.map +1 -1
- package/dist/native/shared/dom.js +171 -10
- package/dist/native/vertical-bars-web.d.ts +3 -0
- package/dist/native/vertical-bars-web.d.ts.map +1 -0
- package/dist/native/vertical-bars-web.js +341 -0
- package/dist/native-integration/index.d.ts +3 -2
- package/dist/native-integration/index.d.ts.map +1 -1
- package/dist/native-integration/index.js +11 -5
- package/dist/transition/index.d.ts +2 -2
- package/dist/transition/index.d.ts.map +1 -1
- package/dist/transition/index.js +1 -1
- package/dist/transition/ios.transition.d.ts.map +1 -1
- package/dist/transition/ios.transition.js +1 -0
- package/dist/utils.d.ts +3 -3
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +1 -1
- package/dist/vertical-bars.d.ts +4 -0
- package/dist/vertical-bars.d.ts.map +1 -0
- package/dist/vertical-bars.js +2 -0
- package/docs/iphone-duo.md +255 -0
- package/docs/migration.md +2 -2
- package/docs/native-ui-shell.md +15 -3
- package/docs/special-markup.md +6 -0
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSearchController.swift +15 -1
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +358 -0
- package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +215 -35
- package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellSnapshot.swift +10 -1
- package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +29 -0
- package/package.json +6 -2
- package/src/index.ts +15 -8
- package/src/native/components/index.ts +12 -3
- package/src/native/components/ion-back-button.ts +8 -2
- package/src/native/components/ion-button.ts +12 -2
- package/src/native/components/ion-buttons.ts +9 -4
- package/src/native/components/ion-fab.ts +4 -4
- package/src/native/components/ion-menu-button.ts +5 -4
- package/src/native/components/searchable-tabs.ts +9 -1
- package/src/native/definitions.ts +40 -3
- package/src/native/index.ts +188 -20
- package/src/native/prehide.ts +255 -0
- package/src/native/runtime.ts +173 -27
- package/src/native/shared/candidate.ts +11 -3
- package/src/native/shared/dom.ts +212 -11
- package/src/native/vertical-bars-web.ts +374 -0
- package/src/native-integration/index.ts +13 -8
- package/src/styles/components/ion-list.scss +2 -2
- package/src/styles/components/ion-menu.scss +2 -2
- package/src/styles/ionic-theme-ios27.scss +1 -0
- package/src/styles/vertical-bars.scss +342 -0
- package/src/transition/index.ts +11 -6
- package/src/transition/ios.transition.ts +1 -0
- package/src/utils.ts +8 -8
- package/src/vertical-bars.ts +3 -0
- package/dist/native/lifecycle.d.ts +0 -4
- package/dist/native/lifecycle.d.ts.map +0 -1
- package/dist/native/lifecycle.js +0 -34
- package/src/native/lifecycle.ts +0 -38
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { frame, isDark, text, visible } from './dom';
|
|
1
|
+
import { frame, isDark, isVerticalBarsSource, text, visible } from './dom';
|
|
2
2
|
import { iconSource } from './icons';
|
|
3
3
|
export const createCandidate = (element, kind, id) => {
|
|
4
4
|
const rect = element.getBoundingClientRect();
|
|
@@ -18,7 +18,8 @@ export const createCandidate = (element, kind, id) => {
|
|
|
18
18
|
};
|
|
19
19
|
};
|
|
20
20
|
export const appendItem = (candidate, child, id, content = child, label = text(child)) => {
|
|
21
|
-
if (!visible(child
|
|
21
|
+
if (!visible(child, isVerticalBarsSource(candidate.element)) ||
|
|
22
|
+
child.querySelector('input, button, a, img, canvas, video, ion-spinner, ion-avatar'))
|
|
22
23
|
return;
|
|
23
24
|
const icons = Array.from(content.querySelectorAll('ion-icon'));
|
|
24
25
|
const directSVGs = Array.from(content.querySelectorAll('svg'));
|
|
@@ -36,7 +37,10 @@ export const appendItem = (candidate, child, id, content = child, label = text(c
|
|
|
36
37
|
id: id(child),
|
|
37
38
|
...frame(child.getBoundingClientRect(), candidate.element.getBoundingClientRect()),
|
|
38
39
|
label,
|
|
39
|
-
accessibilityLabel: child.getAttribute('aria-label') ??
|
|
40
|
+
accessibilityLabel: child.getAttribute('aria-label') ??
|
|
41
|
+
native?.getAttribute('aria-label') ??
|
|
42
|
+
(icons[0]?.getAttribute('aria-hidden') === 'true' ? null : icons[0]?.getAttribute('aria-label')) ??
|
|
43
|
+
label,
|
|
40
44
|
disabled: !!child.disabled ||
|
|
41
45
|
!!candidate.element.disabled ||
|
|
42
46
|
getComputedStyle(child).pointerEvents === 'none',
|
|
@@ -1,10 +1,41 @@
|
|
|
1
1
|
import type { Frame } from '../definitions';
|
|
2
2
|
export declare const marker = "data-native-ui-shell";
|
|
3
|
+
export declare const childNodesOf: (element: Element) => Node[];
|
|
4
|
+
export declare const childElements: (element: Element) => HTMLElement[];
|
|
5
|
+
export declare const prehiddenClass = "ios-theme-native-ui-shell-prehidden";
|
|
6
|
+
export declare const prehideRootClass = "ios-theme-native-ui-shell-prehide";
|
|
7
|
+
export declare const rejectedClass = "ios-theme-native-ui-shell-rejected";
|
|
8
|
+
export declare const verticalBarsBackWebClass = "ios-theme-vertical-bars-back-web-owned";
|
|
9
|
+
export declare const prehideOnlyMutation: (record: MutationRecord) => boolean;
|
|
10
|
+
export declare const withoutPrehide: <T>(element: HTMLElement, read: () => T) => T;
|
|
3
11
|
export declare const isDark: (style: CSSStyleDeclaration) => boolean;
|
|
4
|
-
export declare const excluded = ".ionic-theme-disabled, .ios-theme-disabled, .ios26-disabled, .ion-
|
|
12
|
+
export declare const excluded = ".ionic-theme-disabled, .ios-theme-disabled, .ios26-disabled, .ion-cloned-element, [hidden], [inert], .ion-page-hidden, .ion-page-invisible";
|
|
13
|
+
export declare const setVerticalBarsEnteringPage: (page: HTMLElement, entering: boolean) => void;
|
|
14
|
+
export declare const verticalBarsEnteringPage: (element: HTMLElement) => HTMLElement | undefined;
|
|
15
|
+
export declare const createVerticalBarsPageState: () => {
|
|
16
|
+
isDeparted(element: HTMLElement): boolean;
|
|
17
|
+
lifecycle(event: Event): void;
|
|
18
|
+
cancel(entering?: HTMLElement, leaving?: HTMLElement): void;
|
|
19
|
+
};
|
|
20
|
+
export declare const isDisabledButtonGroupChild: (element: HTMLElement) => boolean;
|
|
21
|
+
export declare const isVerticalBarsSource: (element: HTMLElement) => boolean;
|
|
22
|
+
export declare const isVerticalBarsBackPosition: (element: HTMLElement) => boolean;
|
|
23
|
+
export declare const preferredVerticalBarsBack: <T extends HTMLElement>(elements: T[], doc: Document) => T | undefined;
|
|
24
|
+
export declare const isPermanentlyExcluded: (element: HTMLElement) => boolean;
|
|
25
|
+
export declare const isExcluded: (element: HTMLElement, enteringPage?: HTMLElement) => boolean;
|
|
5
26
|
export declare const isShellDisabled: (element: Element) => boolean;
|
|
27
|
+
export declare const isVerticalBarsToolbarActionShape: (element: HTMLElement) => boolean;
|
|
28
|
+
export declare const setVerticalBarsPlacement: (element: HTMLElement, rail: boolean) => void;
|
|
29
|
+
export declare const clearVerticalBarsPlacement: (element: HTMLElement) => void;
|
|
30
|
+
export declare const verticalBarsOwned: (element: HTMLElement) => boolean;
|
|
31
|
+
export declare const isVerticalBarsToolbarAction: (element: HTMLElement) => boolean;
|
|
32
|
+
export declare const verticalBarsActionCandidate: (element: HTMLElement) => boolean;
|
|
33
|
+
export declare const verticalBarsBackCandidate: (element: HTMLElement) => boolean;
|
|
34
|
+
export declare const verticalBarsToolbarActions: (element: HTMLElement) => HTMLElement[];
|
|
35
|
+
export declare const isVerticalBarsToolbarGroup: (element: HTMLElement) => boolean;
|
|
36
|
+
export declare const activateProjectedElement: (element: HTMLElement) => void;
|
|
6
37
|
export declare const unprojected: <T>(elements: Iterable<HTMLElement>, read: () => T) => T;
|
|
7
|
-
export declare const visible: (element: HTMLElement) => boolean;
|
|
38
|
+
export declare const visible: (element: HTMLElement, allowOutsideViewport?: boolean) => boolean;
|
|
8
39
|
export declare const frame: (rect: DOMRect, origin?: DOMRect) => Frame;
|
|
9
40
|
export declare const text: (element: Element) => string;
|
|
10
41
|
export declare const inFixedToolbar: (element: Element) => boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dom.d.ts","sourceRoot":"","sources":["../../../src/native/shared/dom.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAE5C,eAAO,MAAM,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"dom.d.ts","sourceRoot":"","sources":["../../../src/native/shared/dom.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,9 +1,162 @@
|
|
|
1
1
|
import { fadeMarker } from './crossfade';
|
|
2
2
|
export const marker = 'data-native-ui-shell';
|
|
3
|
-
|
|
4
|
-
export const
|
|
3
|
+
const childNodesGet = typeof Node === 'undefined' ? undefined : Object.getOwnPropertyDescriptor(Node.prototype, 'childNodes')?.get;
|
|
4
|
+
export const childNodesOf = (element) => childNodesGet ? Array.from(childNodesGet.call(element)) : Array.from(element.childNodes);
|
|
5
|
+
export const childElements = (element) => childNodesOf(element).filter((node) => node.nodeType === 1);
|
|
6
|
+
export const prehiddenClass = 'ios-theme-native-ui-shell-prehidden';
|
|
7
|
+
export const prehideRootClass = 'ios-theme-native-ui-shell-prehide';
|
|
8
|
+
export const rejectedClass = 'ios-theme-native-ui-shell-rejected';
|
|
9
|
+
export const verticalBarsBackWebClass = 'ios-theme-vertical-bars-back-web-owned';
|
|
10
|
+
const prehideClasses = new Set([prehiddenClass, prehideRootClass]);
|
|
11
|
+
export const prehideOnlyMutation = (record) => {
|
|
12
|
+
if (record.attributeName !== 'class' || record.oldValue === null)
|
|
13
|
+
return false;
|
|
14
|
+
const withoutPrehide = (value) => value
|
|
15
|
+
.split(/\s+/)
|
|
16
|
+
.filter((name) => name && !prehideClasses.has(name))
|
|
17
|
+
.join(' ');
|
|
18
|
+
return withoutPrehide(record.oldValue) === withoutPrehide(record.target.getAttribute('class') ?? '');
|
|
19
|
+
};
|
|
20
|
+
export const withoutPrehide = (element, read) => {
|
|
21
|
+
const changed = [];
|
|
22
|
+
for (let current = element; current; current = current.parentElement) {
|
|
23
|
+
if (current.classList.contains(prehiddenClass)) {
|
|
24
|
+
current.classList.remove(prehiddenClass);
|
|
25
|
+
changed.push(current);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const root = element.ownerDocument.documentElement;
|
|
29
|
+
if (element.matches('ion-back-button') && root.classList.contains(prehideRootClass)) {
|
|
30
|
+
root.classList.remove(prehideRootClass);
|
|
31
|
+
changed.push(root);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return read();
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
changed.forEach((current) => current.classList.add(current === root ? prehideRootClass : prehiddenClass));
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
export const isDark = (style) => {
|
|
41
|
+
const themeScheme = style.getPropertyValue('--ios27-color-scheme').trim();
|
|
42
|
+
if (themeScheme)
|
|
43
|
+
return themeScheme === 'dark';
|
|
44
|
+
const background = style.getPropertyValue('--ion-background-color-rgb').match(/\d+/g)?.slice(0, 3).map(Number);
|
|
45
|
+
return !!background && background.length === 3 && background[0] * 0.2126 + background[1] * 0.7152 + background[2] * 0.0722 < 128;
|
|
46
|
+
};
|
|
47
|
+
const permanentlyExcluded = '.ionic-theme-disabled, .ios-theme-disabled, .ios26-disabled, .ion-cloned-element, [hidden], [inert]';
|
|
48
|
+
export const excluded = `${permanentlyExcluded}, .ion-page-hidden, .ion-page-invisible`;
|
|
49
|
+
const enteringPages = new WeakSet();
|
|
50
|
+
export const setVerticalBarsEnteringPage = (page, entering) => {
|
|
51
|
+
if (entering)
|
|
52
|
+
enteringPages.add(page);
|
|
53
|
+
else
|
|
54
|
+
enteringPages.delete(page);
|
|
55
|
+
};
|
|
56
|
+
export const verticalBarsEnteringPage = (element) => {
|
|
57
|
+
const page = element.closest('.ion-page-invisible');
|
|
58
|
+
return page && enteringPages.has(page) && page.closest('ion-app.ios-theme-vertical-bars') ? page : undefined;
|
|
59
|
+
};
|
|
60
|
+
export const createVerticalBarsPageState = () => {
|
|
61
|
+
const departed = new WeakSet();
|
|
62
|
+
return {
|
|
63
|
+
isDeparted(element) {
|
|
64
|
+
const page = element.closest('.ion-page');
|
|
65
|
+
return !!page && departed.has(page) && !!element.closest('ion-app.ios-theme-vertical-bars');
|
|
66
|
+
},
|
|
67
|
+
lifecycle(event) {
|
|
68
|
+
const page = event.target && event.target.nodeType === 1 ? event.target : undefined;
|
|
69
|
+
if (!page?.matches('.ion-page'))
|
|
70
|
+
return;
|
|
71
|
+
const entering = event.type === 'ionViewWillEnter';
|
|
72
|
+
if (entering || event.type === 'ionViewDidEnter')
|
|
73
|
+
departed.delete(page);
|
|
74
|
+
else if (event.type === 'ionViewWillLeave' || event.type === 'ionViewDidLeave')
|
|
75
|
+
departed.add(page);
|
|
76
|
+
setVerticalBarsEnteringPage(page, entering);
|
|
77
|
+
},
|
|
78
|
+
cancel(entering, leaving) {
|
|
79
|
+
if (entering) {
|
|
80
|
+
departed.add(entering);
|
|
81
|
+
setVerticalBarsEnteringPage(entering, false);
|
|
82
|
+
}
|
|
83
|
+
if (leaving)
|
|
84
|
+
departed.delete(leaving);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
const disabledButtonGroup = 'ion-buttons:is(.ionic-theme-disabled, .ios-theme-disabled, .ios26-disabled)';
|
|
5
89
|
const shellDisabledSelector = '.ios-theme-shell-disabled';
|
|
90
|
+
export const isDisabledButtonGroupChild = (element) => element.matches('ion-button') && element.parentElement?.matches(disabledButtonGroup) === true;
|
|
91
|
+
const verticalBarsTags = new Set(['ion-button', 'ion-back-button', 'ion-buttons', 'ion-menu-button', 'ion-tab-bar']);
|
|
92
|
+
export const isVerticalBarsSource = (element) => verticalBarsTags.has(element.localName) &&
|
|
93
|
+
(element.matches('ion-tab-bar') || verticalBarsOwned(element)) &&
|
|
94
|
+
!element.closest('ion-menu, ion-modal, ion-popover') &&
|
|
95
|
+
!!element.closest('ion-app.ios-theme-vertical-bars');
|
|
96
|
+
export const isVerticalBarsBackPosition = (element) => {
|
|
97
|
+
if (element.closest('ion-header[collapse], ion-footer[collapse]'))
|
|
98
|
+
return false;
|
|
99
|
+
const page = element.closest('.ion-page');
|
|
100
|
+
if (!page)
|
|
101
|
+
return true;
|
|
102
|
+
const candidates = Array.from(page.querySelectorAll('ion-back-button')).filter((back) => back.closest('.ion-page') === page &&
|
|
103
|
+
!back.matches('.ion-cloned-element') &&
|
|
104
|
+
!back.closest('ion-header[collapse], ion-footer[collapse], ion-menu, ion-modal, ion-popover'));
|
|
105
|
+
const rank = (back) => inFixedToolbar(back) ? 0 : back.closest('ion-header ion-toolbar, ion-footer ion-toolbar') ? 1 : back.closest('ion-toolbar') ? 3 : 2;
|
|
106
|
+
const best = candidates.reduce((winner, back) => (!winner || rank(back) < rank(winner) ? back : winner), undefined);
|
|
107
|
+
return best === element;
|
|
108
|
+
};
|
|
109
|
+
export const preferredVerticalBarsBack = (elements, doc) => {
|
|
110
|
+
const pages = Array.from(doc.querySelectorAll('.ion-page'));
|
|
111
|
+
const pageOrder = (element) => pages.indexOf(element.closest('.ion-page'));
|
|
112
|
+
return elements.sort((a, b) => pageOrder(b) - pageOrder(a) || Number(!!b.closest('ion-header')) - Number(!!a.closest('ion-header')))[0];
|
|
113
|
+
};
|
|
114
|
+
const excludedBy = (element, selector) => {
|
|
115
|
+
const owner = element.closest(selector);
|
|
116
|
+
return !!owner && !(element.parentElement === owner && isDisabledButtonGroupChild(element));
|
|
117
|
+
};
|
|
118
|
+
export const isPermanentlyExcluded = (element) => excludedBy(element, permanentlyExcluded);
|
|
119
|
+
export const isExcluded = (element, enteringPage) => excludedBy(element, `${permanentlyExcluded}, .ion-page-hidden`) || (!!element.closest('.ion-page-invisible') && !enteringPage);
|
|
6
120
|
export const isShellDisabled = (element) => !!element.closest(shellDisabledSelector) || !!element.querySelector(shellDisabledSelector);
|
|
121
|
+
export const isVerticalBarsToolbarActionShape = (element) => element.matches('ion-menu-button') ||
|
|
122
|
+
(element.matches('ion-button') &&
|
|
123
|
+
!!element.querySelector('ion-icon, svg') &&
|
|
124
|
+
!childNodesOf(element).some((node) => node.nodeType === 3 && !!node.textContent?.trim()) &&
|
|
125
|
+
!element.matches('.ion-color, [color]') &&
|
|
126
|
+
['default', 'clear'].includes(element.fill ?? element.getAttribute('fill') ?? 'default'));
|
|
127
|
+
const verticalBarsPlacement = new WeakMap();
|
|
128
|
+
export const setVerticalBarsPlacement = (element, rail) => {
|
|
129
|
+
verticalBarsPlacement.set(element, rail);
|
|
130
|
+
};
|
|
131
|
+
export const clearVerticalBarsPlacement = (element) => {
|
|
132
|
+
verticalBarsPlacement.delete(element);
|
|
133
|
+
};
|
|
134
|
+
export const verticalBarsOwned = (element) => verticalBarsPlacement.get(element) === true;
|
|
135
|
+
export const isVerticalBarsToolbarAction = (element) => verticalBarsOwned(element) && !isExcluded(element, verticalBarsEnteringPage(element)) && !isShellDisabled(element);
|
|
136
|
+
const overlays = 'ion-menu, ion-modal, ion-popover';
|
|
137
|
+
export const verticalBarsActionCandidate = (element) => inFixedToolbar(element) &&
|
|
138
|
+
!element.closest('ion-buttons.ios-theme-horizontal-only, ion-button.ios-theme-horizontal-only') &&
|
|
139
|
+
!isPermanentlyExcluded(element) &&
|
|
140
|
+
!isShellDisabled(element) &&
|
|
141
|
+
!element.closest(overlays);
|
|
142
|
+
export const verticalBarsBackCandidate = (element) => isVerticalBarsBackPosition(element) &&
|
|
143
|
+
!element.closest('ion-buttons.ios-theme-horizontal-only') &&
|
|
144
|
+
!isPermanentlyExcluded(element) &&
|
|
145
|
+
!isShellDisabled(element) &&
|
|
146
|
+
!element.closest(overlays);
|
|
147
|
+
export const verticalBarsToolbarActions = (element) => childElements(element).filter((child) => !child.matches('ion-back-button') && isVerticalBarsToolbarAction(child));
|
|
148
|
+
export const isVerticalBarsToolbarGroup = (element) => {
|
|
149
|
+
return (element.matches('ion-buttons') &&
|
|
150
|
+
verticalBarsOwned(element) &&
|
|
151
|
+
!element.matches('.ionic-theme-disabled, .ios-theme-disabled, .ios26-disabled') &&
|
|
152
|
+
!isShellDisabled(element));
|
|
153
|
+
};
|
|
154
|
+
export const activateProjectedElement = (element) => {
|
|
155
|
+
const target = element.matches('ion-button, ion-back-button, ion-menu-button')
|
|
156
|
+
? element.shadowRoot?.querySelector('[part~="native"]')
|
|
157
|
+
: undefined;
|
|
158
|
+
(target ?? element).click();
|
|
159
|
+
};
|
|
7
160
|
export const unprojected = (elements, read) => {
|
|
8
161
|
const hidden = Array.from(elements).filter((element) => element.hasAttribute(marker));
|
|
9
162
|
hidden.forEach((element) => element.removeAttribute(marker));
|
|
@@ -14,22 +167,29 @@ export const unprojected = (elements, read) => {
|
|
|
14
167
|
hidden.forEach((element) => element.setAttribute(marker, ''));
|
|
15
168
|
}
|
|
16
169
|
};
|
|
17
|
-
|
|
18
|
-
|
|
170
|
+
const readVisible = (element, allowOutsideViewport) => {
|
|
171
|
+
const enteringPage = isVerticalBarsSource(element) ? verticalBarsEnteringPage(element) : undefined;
|
|
172
|
+
if (!element.isConnected || isExcluded(element, enteringPage) || isShellDisabled(element))
|
|
19
173
|
return false;
|
|
20
174
|
for (let current = element; current; current = current.parentElement) {
|
|
21
175
|
const style = getComputedStyle(current);
|
|
22
|
-
if (style.display === 'none' ||
|
|
176
|
+
if (style.display === 'none' ||
|
|
177
|
+
style.visibility !== 'visible' ||
|
|
178
|
+
(Number(style.opacity) === 0 && !current.hasAttribute(fadeMarker) && current !== enteringPage))
|
|
23
179
|
return false;
|
|
24
|
-
if (
|
|
180
|
+
if (!allowOutsideViewport &&
|
|
181
|
+
style.transform !== 'none' &&
|
|
25
182
|
!new DOMMatrixReadOnly(style.transform).isIdentity &&
|
|
26
183
|
current !== element &&
|
|
27
184
|
!current.matches('ion-tab-bar'))
|
|
28
185
|
return false;
|
|
29
186
|
}
|
|
30
187
|
const rect = element.getBoundingClientRect();
|
|
31
|
-
return (rect.width > 0 &&
|
|
188
|
+
return (rect.width > 0 &&
|
|
189
|
+
rect.height > 0 &&
|
|
190
|
+
(allowOutsideViewport || (rect.left >= -1 && rect.top >= -1 && rect.right <= innerWidth + 1 && rect.bottom <= innerHeight + 1)));
|
|
32
191
|
};
|
|
192
|
+
export const visible = (element, allowOutsideViewport = false) => withoutPrehide(element, () => readVisible(element, allowOutsideViewport));
|
|
33
193
|
export const frame = (rect, origin) => ({
|
|
34
194
|
x: rect.x - (origin?.x ?? 0),
|
|
35
195
|
y: rect.y - (origin?.y ?? 0),
|
|
@@ -39,16 +199,17 @@ export const frame = (rect, origin) => ({
|
|
|
39
199
|
export const text = (element) => {
|
|
40
200
|
if (element.matches('ion-icon, svg, ion-badge, .ios27-segment-lens, .ion-cloned-element'))
|
|
41
201
|
return '';
|
|
42
|
-
return
|
|
43
|
-
.map((node) => (node.nodeType ===
|
|
202
|
+
return childNodesOf(element)
|
|
203
|
+
.map((node) => (node.nodeType === 3 ? node.textContent : node.nodeType === 1 ? text(node) : ''))
|
|
44
204
|
.join('')
|
|
45
205
|
.replace(/\s+/g, ' ')
|
|
46
206
|
.trim();
|
|
47
207
|
};
|
|
48
208
|
export const inFixedToolbar = (element) => {
|
|
49
209
|
const edge = element.closest('ion-toolbar')?.parentElement;
|
|
210
|
+
const verticalBars = !!element.closest('ion-app.ios-theme-vertical-bars');
|
|
50
211
|
return (!!edge?.matches('ion-header, ion-footer') &&
|
|
51
212
|
!element.closest('ion-content') &&
|
|
52
213
|
!edge.hasAttribute('collapse') &&
|
|
53
|
-
!edge.matches('.header-collapse-main, .header-collapse-condense'));
|
|
214
|
+
(verticalBars || !edge.matches('.header-collapse-main, .header-collapse-condense')));
|
|
54
215
|
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { NativeUIShellHandle, NativeUIShellOptions } from './definitions';
|
|
2
|
+
export declare const createVerticalBarsWebProjection: (doc: Document, options: NativeUIShellOptions, enabled?: () => boolean) => NativeUIShellHandle;
|
|
3
|
+
//# sourceMappingURL=vertical-bars-web.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vertical-bars-web.d.ts","sourceRoot":"","sources":["../../src/native/vertical-bars-web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,oBAAoB,EAAuB,MAAM,eAAe,CAAC;AAqCpG,eAAO,MAAM,+BAA+B,GAC1C,KAAK,QAAQ,EACb,SAAS,oBAAoB,EAC7B,UAAS,MAAM,OAAoB,KAClC,mBA4UF,CAAC"}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { VERTICAL_BARS_TRANSITION_CANCELED } from '../native-integration';
|
|
2
|
+
import { activateProjectedElement, createVerticalBarsPageState, verticalBarsActionCandidate, verticalBarsBackCandidate, verticalBarsEnteringPage, verticalBarsToolbarActions, isExcluded, isVerticalBarsToolbarGroup, preferredVerticalBarsBack, verticalBarsOwned, marker, prehideOnlyMutation, prehiddenClass, unprojected, withoutPrehide, } from './shared/dom';
|
|
3
|
+
const backProjectionClass = 'ios-theme-vertical-bars-back-button-projection';
|
|
4
|
+
const toolbarProjectionClass = 'ios-theme-vertical-bars-toolbar-projection';
|
|
5
|
+
const readyClass = 'ios-theme-vertical-bars-toolbar-ready';
|
|
6
|
+
const toolbarControlSize = 46;
|
|
7
|
+
const toolbarControlGap = 10;
|
|
8
|
+
export const createVerticalBarsWebProjection = (doc, options, enabled = () => true) => {
|
|
9
|
+
const win = doc.defaultView;
|
|
10
|
+
if (options.controls !== undefined && options.controls.toolbar !== true)
|
|
11
|
+
return {
|
|
12
|
+
getStatus: () => ({ state: 'web', projected: 0, updates: 0 }),
|
|
13
|
+
suspend: async () => ({ resume: async () => { } }),
|
|
14
|
+
destroy: async () => { },
|
|
15
|
+
};
|
|
16
|
+
let root;
|
|
17
|
+
let backSource;
|
|
18
|
+
let backProjection;
|
|
19
|
+
let previousBackAriaHidden = null;
|
|
20
|
+
let toolbarProjections = [];
|
|
21
|
+
let suspended = 0;
|
|
22
|
+
let stopped = false;
|
|
23
|
+
let frame = 0;
|
|
24
|
+
let updates = 0;
|
|
25
|
+
let observingVerticalBars = false;
|
|
26
|
+
const verticalBarsPages = createVerticalBarsPageState();
|
|
27
|
+
let sourceObserver;
|
|
28
|
+
let waiters = [];
|
|
29
|
+
const listeners = new AbortController();
|
|
30
|
+
const verticalBarsRoot = () => doc.querySelector('ion-app.ios-theme-vertical-bars');
|
|
31
|
+
const projectedSources = () => [backSource, ...toolbarProjections.flatMap(({ actions }) => actions.map(({ source }) => source))].filter((source) => !!source);
|
|
32
|
+
const isRendered = (element) => withoutPrehide(element, () => {
|
|
33
|
+
const style = getComputedStyle(element);
|
|
34
|
+
const rect = element.getBoundingClientRect();
|
|
35
|
+
return element.isConnected && style.display !== 'none' && style.visibility === 'visible' && rect.width > 0 && rect.height > 0;
|
|
36
|
+
});
|
|
37
|
+
const inEligibleToolbar = (element) => !!verticalBarsRoot()?.contains(element) &&
|
|
38
|
+
verticalBarsActionCandidate(element) &&
|
|
39
|
+
!isExcluded(element, verticalBarsEnteringPage(element)) &&
|
|
40
|
+
!verticalBarsPages.isDeparted(element);
|
|
41
|
+
const isEligibleBack = (element) => !!verticalBarsRoot()?.contains(element) &&
|
|
42
|
+
verticalBarsOwned(element) &&
|
|
43
|
+
verticalBarsBackCandidate(element) &&
|
|
44
|
+
!isExcluded(element, verticalBarsEnteringPage(element)) &&
|
|
45
|
+
!verticalBarsPages.isDeparted(element) &&
|
|
46
|
+
unprojected(projectedSources(), () => isRendered(element));
|
|
47
|
+
const pageOrder = (element) => Array.from(doc.querySelectorAll('.ion-page')).indexOf(element.closest('.ion-page'));
|
|
48
|
+
const findBack = () => {
|
|
49
|
+
const candidates = Array.from(doc.querySelectorAll(`ion-back-button:not(.${backProjectionClass})`)).filter(isEligibleBack);
|
|
50
|
+
return preferredVerticalBarsBack(candidates, doc);
|
|
51
|
+
};
|
|
52
|
+
const findToolbarGroups = () => {
|
|
53
|
+
const candidates = Array.from(doc.querySelectorAll(`ion-buttons:not(.${toolbarProjectionClass})`)).flatMap((group) => {
|
|
54
|
+
const actions = verticalBarsToolbarActions(group).filter((action) => unprojected(projectedSources(), () => isRendered(action)));
|
|
55
|
+
if (!actions.length)
|
|
56
|
+
return [];
|
|
57
|
+
if (!isVerticalBarsToolbarGroup(group))
|
|
58
|
+
return actions.filter(inEligibleToolbar).map((action) => ({ group, actions: [action] }));
|
|
59
|
+
return inEligibleToolbar(group) && unprojected(projectedSources(), () => isRendered(group)) ? [{ group, actions }] : [];
|
|
60
|
+
});
|
|
61
|
+
const highestPage = Math.max(...candidates.map(({ group }) => pageOrder(group)));
|
|
62
|
+
return candidates.filter(({ group }) => pageOrder(group) === highestPage);
|
|
63
|
+
};
|
|
64
|
+
const isCurrentToolbarAction = (source) => findToolbarGroups().some(({ actions }) => actions.includes(source));
|
|
65
|
+
const restore = () => {
|
|
66
|
+
backProjection?.remove();
|
|
67
|
+
backProjection = undefined;
|
|
68
|
+
if (backSource) {
|
|
69
|
+
backSource.removeAttribute(marker);
|
|
70
|
+
if (previousBackAriaHidden === null)
|
|
71
|
+
backSource.removeAttribute('aria-hidden');
|
|
72
|
+
else
|
|
73
|
+
backSource.setAttribute('aria-hidden', previousBackAriaHidden);
|
|
74
|
+
backSource.dispatchEvent(new CustomEvent('nativeUIShellChange'));
|
|
75
|
+
}
|
|
76
|
+
backSource = undefined;
|
|
77
|
+
previousBackAriaHidden = null;
|
|
78
|
+
for (const { projection, actions } of toolbarProjections) {
|
|
79
|
+
projection.remove();
|
|
80
|
+
for (const { source, previousAriaHidden } of actions) {
|
|
81
|
+
source.removeAttribute(marker);
|
|
82
|
+
if (previousAriaHidden === null)
|
|
83
|
+
source.removeAttribute('aria-hidden');
|
|
84
|
+
else
|
|
85
|
+
source.setAttribute('aria-hidden', previousAriaHidden);
|
|
86
|
+
source.dispatchEvent(new CustomEvent('nativeUIShellChange'));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
toolbarProjections = [];
|
|
90
|
+
sourceObserver?.disconnect();
|
|
91
|
+
sourceObserver = undefined;
|
|
92
|
+
root?.classList.remove(readyClass);
|
|
93
|
+
root = undefined;
|
|
94
|
+
};
|
|
95
|
+
const copyAttributes = (target, original) => {
|
|
96
|
+
for (const name of ['id', 'slot', marker, 'aria-hidden'])
|
|
97
|
+
target.removeAttribute(name);
|
|
98
|
+
for (const attribute of Array.from(target.attributes))
|
|
99
|
+
if (!['class', marker].includes(attribute.name) && !original.hasAttribute(attribute.name))
|
|
100
|
+
target.removeAttribute(attribute.name);
|
|
101
|
+
for (const attribute of Array.from(original.attributes))
|
|
102
|
+
if (!['id', 'slot', marker, 'aria-hidden'].includes(attribute.name))
|
|
103
|
+
target.setAttribute(attribute.name, attribute.value);
|
|
104
|
+
target.classList.remove(prehiddenClass);
|
|
105
|
+
};
|
|
106
|
+
const syncBack = (target, original) => {
|
|
107
|
+
copyAttributes(target, original);
|
|
108
|
+
target.classList.add(backProjectionClass, 'ion-cloned-element');
|
|
109
|
+
target.disabled = original.disabled;
|
|
110
|
+
target.defaultHref = original.defaultHref;
|
|
111
|
+
target.icon = original.icon;
|
|
112
|
+
target.color = original.color;
|
|
113
|
+
target.mode = original.mode;
|
|
114
|
+
target.text = '';
|
|
115
|
+
};
|
|
116
|
+
const syncAction = (target, original) => {
|
|
117
|
+
copyAttributes(target, original);
|
|
118
|
+
target.classList.add('ios-theme-vertical-bars-toolbar-action', 'ion-cloned-element');
|
|
119
|
+
const label = original.getAttribute('aria-label') ?? original.textContent?.trim();
|
|
120
|
+
if (label)
|
|
121
|
+
target.setAttribute('aria-label', label);
|
|
122
|
+
if ('disabled' in original)
|
|
123
|
+
target.disabled = original.disabled;
|
|
124
|
+
if (original.matches('ion-button')) {
|
|
125
|
+
const icon = original.querySelector('ion-icon, svg');
|
|
126
|
+
const clonedIcon = icon?.cloneNode(true);
|
|
127
|
+
clonedIcon?.setAttribute('slot', 'icon-only');
|
|
128
|
+
target.replaceChildren(...(clonedIcon ? [clonedIcon] : []));
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const sameSources = (back, groups) => back === backSource &&
|
|
132
|
+
groups.length === toolbarProjections.length &&
|
|
133
|
+
groups.every(({ group, actions }, index) => {
|
|
134
|
+
const current = toolbarProjections[index];
|
|
135
|
+
return (group === current.source &&
|
|
136
|
+
actions.length === current.actions.length &&
|
|
137
|
+
actions.every((action, actionIndex) => action === current.actions[actionIndex].source));
|
|
138
|
+
});
|
|
139
|
+
const syncExisting = () => {
|
|
140
|
+
if (backSource && backProjection)
|
|
141
|
+
syncBack(backProjection, backSource);
|
|
142
|
+
let topOffset = backSource ? toolbarControlSize + toolbarControlGap : 0;
|
|
143
|
+
for (const { projection, source, actions } of toolbarProjections) {
|
|
144
|
+
if (actions.length === 1 && !projection.matches('ion-buttons'))
|
|
145
|
+
syncAction(projection, actions[0].source);
|
|
146
|
+
else
|
|
147
|
+
copyAttributes(projection, source);
|
|
148
|
+
projection.classList.add(toolbarProjectionClass, 'ion-cloned-element');
|
|
149
|
+
projection.style.setProperty('--ios-theme-vertical-bars-toolbar-offset', `${topOffset}px`);
|
|
150
|
+
if (projection.matches('ion-buttons'))
|
|
151
|
+
actions.forEach(({ source: action, projection: clone }) => syncAction(clone, action));
|
|
152
|
+
topOffset += actions.length * toolbarControlSize + toolbarControlGap;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const project = (nextBack, groups) => {
|
|
156
|
+
root = verticalBarsRoot();
|
|
157
|
+
let topOffset = 0;
|
|
158
|
+
if (nextBack) {
|
|
159
|
+
backSource = nextBack;
|
|
160
|
+
previousBackAriaHidden = nextBack.getAttribute('aria-hidden');
|
|
161
|
+
backProjection = nextBack.cloneNode(false);
|
|
162
|
+
syncBack(backProjection, nextBack);
|
|
163
|
+
backProjection.addEventListener('click', (event) => {
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
event.stopImmediatePropagation();
|
|
166
|
+
if (backSource && backSource === findBack())
|
|
167
|
+
activateProjectedElement(backSource);
|
|
168
|
+
}, { capture: true });
|
|
169
|
+
root.append(backProjection);
|
|
170
|
+
nextBack.setAttribute(marker, '');
|
|
171
|
+
nextBack.setAttribute('aria-hidden', 'true');
|
|
172
|
+
nextBack.dispatchEvent(new CustomEvent('nativeUIShellChange'));
|
|
173
|
+
topOffset = toolbarControlSize + toolbarControlGap;
|
|
174
|
+
}
|
|
175
|
+
for (const { group, actions: sources } of groups) {
|
|
176
|
+
const projection = (sources.length === 1 ? sources[0] : group).cloneNode(false);
|
|
177
|
+
if (sources.length === 1)
|
|
178
|
+
syncAction(projection, sources[0]);
|
|
179
|
+
else
|
|
180
|
+
copyAttributes(projection, group);
|
|
181
|
+
projection.classList.add(toolbarProjectionClass, 'ion-cloned-element');
|
|
182
|
+
projection.style.setProperty('--ios-theme-vertical-bars-toolbar-offset', `${topOffset}px`);
|
|
183
|
+
const actions = sources.map((source) => {
|
|
184
|
+
const clone = sources.length === 1 ? projection : source.cloneNode(false);
|
|
185
|
+
if (sources.length > 1)
|
|
186
|
+
syncAction(clone, source);
|
|
187
|
+
clone.addEventListener('click', (event) => {
|
|
188
|
+
event.preventDefault();
|
|
189
|
+
event.stopImmediatePropagation();
|
|
190
|
+
if (isCurrentToolbarAction(source))
|
|
191
|
+
activateProjectedElement(source);
|
|
192
|
+
}, { capture: true });
|
|
193
|
+
if (sources.length > 1)
|
|
194
|
+
projection.append(clone);
|
|
195
|
+
const previousAriaHidden = source.getAttribute('aria-hidden');
|
|
196
|
+
source.setAttribute(marker, '');
|
|
197
|
+
source.setAttribute('aria-hidden', 'true');
|
|
198
|
+
source.dispatchEvent(new CustomEvent('nativeUIShellChange'));
|
|
199
|
+
return { source, projection: clone, previousAriaHidden };
|
|
200
|
+
});
|
|
201
|
+
root.append(projection);
|
|
202
|
+
toolbarProjections.push({ source: group, projection, actions });
|
|
203
|
+
topOffset += actions.length * toolbarControlSize + toolbarControlGap;
|
|
204
|
+
}
|
|
205
|
+
if (nextBack || toolbarProjections.length)
|
|
206
|
+
root.classList.add(readyClass);
|
|
207
|
+
sourceObserver = new MutationObserver((records) => {
|
|
208
|
+
if (records.some((record) => record.type !== 'attributes' || (![marker, 'aria-hidden'].includes(record.attributeName ?? '') && !prehideOnlyMutation(record))))
|
|
209
|
+
schedule();
|
|
210
|
+
});
|
|
211
|
+
if (nextBack?.shadowRoot)
|
|
212
|
+
sourceObserver.observe(nextBack.shadowRoot, { subtree: true, childList: true, attributes: true, attributeOldValue: true });
|
|
213
|
+
for (const { group } of groups)
|
|
214
|
+
sourceObserver.observe(group, { subtree: true, childList: true, attributes: true, attributeOldValue: true });
|
|
215
|
+
};
|
|
216
|
+
const performUpdate = () => {
|
|
217
|
+
frame = 0;
|
|
218
|
+
const currentRoot = verticalBarsRoot();
|
|
219
|
+
if (observingVerticalBars !== !!currentRoot) {
|
|
220
|
+
observingVerticalBars = !!currentRoot;
|
|
221
|
+
observer.disconnect();
|
|
222
|
+
observer.observe(doc.documentElement, {
|
|
223
|
+
subtree: true,
|
|
224
|
+
childList: true,
|
|
225
|
+
attributes: true,
|
|
226
|
+
attributeOldValue: true,
|
|
227
|
+
attributeFilter: observingVerticalBars ? undefined : ['class'],
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
if (stopped || suspended || !currentRoot || !enabled())
|
|
231
|
+
return restore();
|
|
232
|
+
const nextBack = findBack();
|
|
233
|
+
const groups = findToolbarGroups();
|
|
234
|
+
if (!nextBack && !groups.length)
|
|
235
|
+
return restore();
|
|
236
|
+
if (sameSources(nextBack, groups))
|
|
237
|
+
return syncExisting();
|
|
238
|
+
restore();
|
|
239
|
+
project(nextBack, groups);
|
|
240
|
+
updates++;
|
|
241
|
+
};
|
|
242
|
+
const update = () => {
|
|
243
|
+
try {
|
|
244
|
+
performUpdate();
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
restore();
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
const pending = waiters;
|
|
251
|
+
waiters = [];
|
|
252
|
+
pending.forEach((resolve) => resolve());
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
const schedule = () => {
|
|
256
|
+
if (stopped)
|
|
257
|
+
return Promise.resolve();
|
|
258
|
+
const done = new Promise((resolve) => waiters.push(resolve));
|
|
259
|
+
if (!frame)
|
|
260
|
+
frame = win.requestAnimationFrame(update);
|
|
261
|
+
return done;
|
|
262
|
+
};
|
|
263
|
+
const observer = new MutationObserver((records) => {
|
|
264
|
+
const insideProjection = (target) => {
|
|
265
|
+
const element = target instanceof Element ? target : target.parentNode instanceof Element ? target.parentNode : undefined;
|
|
266
|
+
const rootNode = target.getRootNode();
|
|
267
|
+
const shadowHost = rootNode instanceof ShadowRoot ? rootNode.host : undefined;
|
|
268
|
+
return !![element, shadowHost].some((candidate) => candidate?.closest(`.${backProjectionClass}, .${toolbarProjectionClass}`));
|
|
269
|
+
};
|
|
270
|
+
const verticalBarsChanged = records.some((record) => (record.type === 'attributes' && record.target.matches('ion-app')) ||
|
|
271
|
+
(record.type === 'childList' &&
|
|
272
|
+
Array.from(record.addedNodes).some((node) => node instanceof Element &&
|
|
273
|
+
(node.matches('ion-app.ios-theme-vertical-bars') || !!node.querySelector('ion-app.ios-theme-vertical-bars')))));
|
|
274
|
+
if ((observingVerticalBars &&
|
|
275
|
+
records.some((record) => record.attributeName !== marker && !prehideOnlyMutation(record) && !insideProjection(record.target))) ||
|
|
276
|
+
(!observingVerticalBars && verticalBarsChanged && records.some((record) => !prehideOnlyMutation(record))))
|
|
277
|
+
schedule();
|
|
278
|
+
});
|
|
279
|
+
observer.observe(doc.documentElement, {
|
|
280
|
+
subtree: true,
|
|
281
|
+
childList: true,
|
|
282
|
+
attributes: true,
|
|
283
|
+
attributeOldValue: true,
|
|
284
|
+
attributeFilter: ['class'],
|
|
285
|
+
});
|
|
286
|
+
const pageLifecycle = (event) => {
|
|
287
|
+
const page = event.target;
|
|
288
|
+
if (!(page instanceof HTMLElement) || !page.matches('.ion-page'))
|
|
289
|
+
return;
|
|
290
|
+
if (event.type === VERTICAL_BARS_TRANSITION_CANCELED) {
|
|
291
|
+
const entering = event.detail?.entering;
|
|
292
|
+
verticalBarsPages.cancel(entering, page);
|
|
293
|
+
}
|
|
294
|
+
else
|
|
295
|
+
verticalBarsPages.lifecycle(event);
|
|
296
|
+
if (verticalBarsRoot())
|
|
297
|
+
schedule();
|
|
298
|
+
};
|
|
299
|
+
for (const name of ['ionViewWillEnter', 'ionViewWillLeave', 'ionViewDidEnter', 'ionViewDidLeave'])
|
|
300
|
+
doc.addEventListener(name, pageLifecycle, { capture: true, signal: listeners.signal });
|
|
301
|
+
doc.addEventListener(VERTICAL_BARS_TRANSITION_CANCELED, pageLifecycle, { capture: true, signal: listeners.signal });
|
|
302
|
+
for (const name of ['ionModalWillPresent', 'ionModalDidDismiss'])
|
|
303
|
+
doc.addEventListener(name, schedule, { capture: true, signal: listeners.signal });
|
|
304
|
+
win.addEventListener('nativeUIShellRefresh', schedule, { signal: listeners.signal });
|
|
305
|
+
schedule();
|
|
306
|
+
return {
|
|
307
|
+
getStatus: () => ({
|
|
308
|
+
state: stopped ? 'stopped' : 'web',
|
|
309
|
+
projected: Number(!!backSource) + toolbarProjections.reduce((count, group) => count + group.actions.length, 0),
|
|
310
|
+
updates,
|
|
311
|
+
}),
|
|
312
|
+
async suspend() {
|
|
313
|
+
suspended++;
|
|
314
|
+
restore();
|
|
315
|
+
let resumed = false;
|
|
316
|
+
return {
|
|
317
|
+
async resume() {
|
|
318
|
+
if (resumed)
|
|
319
|
+
return;
|
|
320
|
+
resumed = true;
|
|
321
|
+
suspended = Math.max(0, suspended - 1);
|
|
322
|
+
if (!suspended)
|
|
323
|
+
await schedule();
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
},
|
|
327
|
+
async destroy() {
|
|
328
|
+
if (stopped)
|
|
329
|
+
return;
|
|
330
|
+
stopped = true;
|
|
331
|
+
win.cancelAnimationFrame(frame);
|
|
332
|
+
frame = 0;
|
|
333
|
+
waiters.forEach((resolve) => resolve());
|
|
334
|
+
waiters = [];
|
|
335
|
+
observer.disconnect();
|
|
336
|
+
sourceObserver?.disconnect();
|
|
337
|
+
listeners.abort();
|
|
338
|
+
restore();
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
};
|