@rdlabo/ionic-theme-ios26 0.3.4 → 0.4.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 +21 -10
- package/dist/css/components/ion-segment.css +1 -1
- package/dist/css/components/ion-tabs.css +1 -1
- package/dist/css/ionic-theme-ios26.css +1 -1
- package/dist/css/md-remove-ios-class-effect.css +1 -1
- package/dist/gestures/gestures.d.ts +3 -0
- package/dist/gestures/gestures.d.ts.map +1 -0
- package/dist/gestures/gestures.js +240 -0
- package/dist/gestures/index.d.ts +3 -0
- package/dist/gestures/index.d.ts.map +1 -0
- package/dist/gestures/index.js +240 -0
- package/dist/gestures/interfaces.d.ts +10 -0
- package/dist/gestures/interfaces.d.ts.map +1 -0
- package/dist/gestures/interfaces.js +1 -0
- package/dist/gestures/utils.d.ts +3 -0
- package/dist/gestures/utils.d.ts.map +1 -0
- package/dist/gestures/utils.js +24 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -206
- package/dist/utils.d.ts +3 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +24 -0
- package/package.json +2 -2
- package/src/gestures/index.ts +290 -0
- package/src/gestures/interfaces.ts +10 -0
- package/src/gestures/utils.ts +28 -0
- package/src/index.ts +18 -241
- package/src/styles/components/ion-segment.scss +106 -0
- package/src/{components → styles/components}/ion-tabs.scss +2 -2
- package/src/{md-remove-ios-class-effect.scss → styles/md-remove-ios-class-effect.scss} +2 -2
- package/src/components/ion-segment.scss +0 -64
- /package/src/{components → styles/components}/ion-action-sheet.scss +0 -0
- /package/src/{components → styles/components}/ion-alert.scss +0 -0
- /package/src/{components → styles/components}/ion-breadcrumbs.scss +0 -0
- /package/src/{components → styles/components}/ion-button.scss +0 -0
- /package/src/{components → styles/components}/ion-card.scss +0 -0
- /package/src/{components → styles/components}/ion-chip.scss +0 -0
- /package/src/{components → styles/components}/ion-content.scss +0 -0
- /package/src/{components → styles/components}/ion-datetime.scss +0 -0
- /package/src/{components → styles/components}/ion-fab.scss +0 -0
- /package/src/{components → styles/components}/ion-list.scss +0 -0
- /package/src/{components → styles/components}/ion-loading.scss +0 -0
- /package/src/{components → styles/components}/ion-modal.scss +0 -0
- /package/src/{components → styles/components}/ion-picker.scss +0 -0
- /package/src/{components → styles/components}/ion-popover.scss +0 -0
- /package/src/{components → styles/components}/ion-range.scss +0 -0
- /package/src/{components → styles/components}/ion-searchbar.scss +0 -0
- /package/src/{components → styles/components}/ion-toast.scss +0 -0
- /package/src/{components → styles/components}/ion-toggle.scss +0 -0
- /package/src/{components → styles/components}/ion-toolbar.scss +0 -0
- /package/src/{default-variables.scss → styles/default-variables.scss} +0 -0
- /package/src/{ionic-theme-ios26-dark-always.scss → styles/ionic-theme-ios26-dark-always.scss} +0 -0
- /package/src/{ionic-theme-ios26-dark-class.scss → styles/ionic-theme-ios26-dark-class.scss} +0 -0
- /package/src/{ionic-theme-ios26-dark-system.scss → styles/ionic-theme-ios26-dark-system.scss} +0 -0
- /package/src/{ionic-theme-ios26.scss → styles/ionic-theme-ios26.scss} +0 -0
- /package/src/{md-ion-list-inset.scss → styles/md-ion-list-inset.scss} +0 -0
- /package/src/{utils → styles/utils}/api.scss +0 -0
- /package/src/{utils → styles/utils}/dark/ion-button.scss +0 -0
- /package/src/{utils → styles/utils}/dark/ion-fab.scss +0 -0
- /package/src/{utils → styles/utils}/dark/ion-tabs.scss +0 -0
- /package/src/{utils → styles/utils}/theme-dark.scss +0 -0
- /package/src/{utils → styles/utils}/theme-list-inset.scss +0 -0
- /package/src/{utils → styles/utils}/translucent.scss +0 -0
package/dist/index.js
CHANGED
|
@@ -1,208 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const MiddleScale = 'scale(1.2)';
|
|
11
|
-
const MaxScale = 'scale(1.3)';
|
|
12
|
-
const OverScale = 'scale(1.4)';
|
|
13
|
-
ionTabBar.addEventListener('pointerdown', () => clearActivated());
|
|
14
|
-
const createTabButtonGesture = () => {
|
|
15
|
-
ionTabBar.classList.add('tab-bar-ios26-effect');
|
|
16
|
-
gesture = createGesture({
|
|
17
|
-
el: ionTabBar,
|
|
18
|
-
threshold: 0,
|
|
19
|
-
gestureName: GestureName,
|
|
20
|
-
onStart: (event) => {
|
|
21
|
-
enterTabButtonAnimation(event)?.play();
|
|
22
|
-
},
|
|
23
|
-
onMove: (event) => {
|
|
24
|
-
moveTabButtonAnimation(event)?.play();
|
|
25
|
-
},
|
|
26
|
-
onEnd: (event) => {
|
|
27
|
-
leaveTabButtonAnimation(event).then((animation) => animation?.play());
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
gesture.enable(true);
|
|
31
|
-
};
|
|
32
|
-
createTabButtonGesture();
|
|
33
|
-
const clearActivated = (isAfterAddWrite = false) => {
|
|
34
|
-
if (currentTouchedButton) {
|
|
35
|
-
tabEffectEl.style.display = 'none';
|
|
36
|
-
tabEffectEl.innerHTML = '';
|
|
37
|
-
tabEffectEl.style.top = 'auto';
|
|
38
|
-
tabEffectEl.style.top = 'left';
|
|
39
|
-
tabEffectEl.style.transform = 'none';
|
|
40
|
-
currentTouchedButton.classList.remove('ion-activated');
|
|
41
|
-
if (isAfterAddWrite) {
|
|
42
|
-
currentTouchedButton.click();
|
|
43
|
-
}
|
|
44
|
-
if (!isAfterAddWrite) {
|
|
45
|
-
gesture.destroy();
|
|
46
|
-
createTabButtonGesture();
|
|
47
|
-
}
|
|
48
|
-
currentTouchedButton = null;
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
const getTransform = (detailCurrentX, tabSelectedActual) => {
|
|
52
|
-
const diff = -2;
|
|
53
|
-
const currentX = detailCurrentX - tabSelectedActual.clientWidth / 2;
|
|
54
|
-
const maxLeft = tabSelectedActual.getBoundingClientRect().left + diff;
|
|
55
|
-
const maxRight = tabSelectedActual.getBoundingClientRect().right - diff - tabSelectedActual.clientWidth;
|
|
56
|
-
if (maxLeft < currentX && currentX < maxRight) {
|
|
57
|
-
return `translate3d(${currentX}px, ${tabEffectElY}px, 0)`;
|
|
58
|
-
}
|
|
59
|
-
if (maxLeft > currentX) {
|
|
60
|
-
return `translate3d(${maxLeft}px, ${tabEffectElY}px, 0)`;
|
|
61
|
-
}
|
|
62
|
-
return `translate3d(${maxRight}px, ${tabEffectElY}px, 0)`;
|
|
63
|
-
};
|
|
64
|
-
const enterTabButtonAnimation = (detail) => {
|
|
65
|
-
currentTouchedButton = detail.event.target.closest('ion-tab-button');
|
|
66
|
-
const tabSelectedActual = ionTabBar.querySelector('ion-tab-button.tab-selected');
|
|
67
|
-
if (tabSelectedActual === null || currentTouchedButton === null) {
|
|
68
|
-
return undefined;
|
|
69
|
-
}
|
|
70
|
-
tabEffectElY = tabSelectedActual.getBoundingClientRect().top;
|
|
71
|
-
const startTransform = getTransform(tabSelectedActual.getBoundingClientRect().left + tabSelectedActual.clientWidth / 2, tabSelectedActual);
|
|
72
|
-
const middleTransform = getTransform((tabSelectedActual.getBoundingClientRect().left + tabSelectedActual.clientWidth / 2 + detail.currentX) / 2, currentTouchedButton);
|
|
73
|
-
const endTransform = getTransform(detail.currentX, currentTouchedButton);
|
|
74
|
-
const tabButtonAnimation = createAnimation();
|
|
75
|
-
tabButtonAnimation
|
|
76
|
-
.addElement(tabEffectEl)
|
|
77
|
-
.delay(70)
|
|
78
|
-
.beforeStyles({
|
|
79
|
-
width: `${tabSelectedActual.clientWidth}px`,
|
|
80
|
-
height: `${tabSelectedActual.clientHeight}px`,
|
|
81
|
-
display: 'block',
|
|
82
|
-
})
|
|
83
|
-
.beforeAddWrite(() => {
|
|
84
|
-
tabSelectedActual.childNodes.forEach((node) => {
|
|
85
|
-
tabEffectEl.appendChild(node.cloneNode(true));
|
|
86
|
-
});
|
|
87
|
-
currentTouchedButton.classList.add('ion-activated');
|
|
88
|
-
});
|
|
89
|
-
if (currentTouchedButton === tabSelectedActual) {
|
|
90
|
-
tabButtonAnimation
|
|
91
|
-
.keyframes([
|
|
92
|
-
{
|
|
93
|
-
transform: `${startTransform} ${MinScale}`,
|
|
94
|
-
opacity: 1,
|
|
95
|
-
offset: 0,
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
transform: `${middleTransform} ${MiddleScale}`,
|
|
99
|
-
opacity: 1,
|
|
100
|
-
offset: 0.6,
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
transform: `${endTransform} ${MaxScale}`,
|
|
104
|
-
opacity: 1,
|
|
105
|
-
offset: 1,
|
|
106
|
-
},
|
|
107
|
-
])
|
|
108
|
-
.duration(120);
|
|
109
|
-
gestureMoveStartTime = detail.currentTime + 120;
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
tabButtonAnimation
|
|
113
|
-
.keyframes([
|
|
114
|
-
{
|
|
115
|
-
transform: `${startTransform} ${MinScale}`,
|
|
116
|
-
opacity: 1,
|
|
117
|
-
offset: 0,
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
transform: `${middleTransform} ${MiddleScale}`,
|
|
121
|
-
opacity: 1,
|
|
122
|
-
offset: 0.4,
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
transform: `${endTransform} ${MaxScale}`,
|
|
126
|
-
opacity: 1,
|
|
127
|
-
offset: 0.55,
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
transform: `${endTransform} ${OverScale}`,
|
|
131
|
-
opacity: 1,
|
|
132
|
-
offset: 0.75,
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
transform: `${endTransform} ${MaxScale}`,
|
|
136
|
-
opacity: 1,
|
|
137
|
-
offset: 1,
|
|
138
|
-
},
|
|
139
|
-
])
|
|
140
|
-
.duration(480);
|
|
141
|
-
gestureMoveStartTime = detail.currentTime + 480;
|
|
142
|
-
}
|
|
143
|
-
return tabButtonAnimation;
|
|
144
|
-
};
|
|
145
|
-
const moveTabButtonAnimation = (detail) => {
|
|
146
|
-
if (gestureMoveStartTime) {
|
|
147
|
-
if (detail.currentTime < gestureMoveStartTime) {
|
|
148
|
-
return undefined;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
const tabSelectedActual = ionTabBar.querySelector('ion-tab-button.tab-selected');
|
|
152
|
-
if (tabSelectedActual === null || currentTouchedButton === null) {
|
|
153
|
-
return undefined;
|
|
154
|
-
}
|
|
155
|
-
const transform = getTransform(detail.currentX, currentTouchedButton);
|
|
156
|
-
const tabButtonAnimation = createAnimation();
|
|
157
|
-
tabButtonAnimation.addElement(tabEffectEl);
|
|
158
|
-
tabButtonAnimation.duration(50).keyframes([
|
|
159
|
-
{
|
|
160
|
-
transform: `${transform} ${MaxScale}`,
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
transform: `${transform} ${MaxScale}`,
|
|
164
|
-
},
|
|
165
|
-
]);
|
|
166
|
-
return tabButtonAnimation;
|
|
167
|
-
};
|
|
168
|
-
const leaveTabButtonAnimation = async (detail) => {
|
|
169
|
-
if (gestureMoveStartTime) {
|
|
170
|
-
if (detail.currentTime < gestureMoveStartTime) {
|
|
171
|
-
await new Promise((resolve) => setTimeout(resolve, gestureMoveStartTime - detail.currentTime));
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
const tabSelectedActual = ionTabBar.querySelector('ion-tab-button.tab-selected');
|
|
175
|
-
if (tabSelectedActual === null || currentTouchedButton === null) {
|
|
176
|
-
return undefined;
|
|
177
|
-
}
|
|
178
|
-
const endTransform = getTransform(currentTouchedButton.getBoundingClientRect().left + currentTouchedButton.clientWidth / 2, currentTouchedButton);
|
|
179
|
-
const tabButtonAnimation = createAnimation();
|
|
180
|
-
tabButtonAnimation.addElement(tabEffectEl);
|
|
181
|
-
tabButtonAnimation
|
|
182
|
-
.onFinish(() => clearActivated(true))
|
|
183
|
-
.duration(50)
|
|
184
|
-
.keyframes([
|
|
185
|
-
{
|
|
186
|
-
transform: `${endTransform} ${MaxScale}`,
|
|
187
|
-
opacity: 1,
|
|
188
|
-
},
|
|
189
|
-
{
|
|
190
|
-
transform: `${endTransform} ${MinScale}`,
|
|
191
|
-
opacity: 0,
|
|
192
|
-
},
|
|
193
|
-
]);
|
|
194
|
-
return tabButtonAnimation;
|
|
195
|
-
};
|
|
196
|
-
return gesture;
|
|
1
|
+
import { registerEffect } from './gestures';
|
|
2
|
+
export * from './gestures/interfaces';
|
|
3
|
+
export const registerTabBarEffect = (targetElement) => {
|
|
4
|
+
return registerEffect(targetElement, 'ion-tab-button', 'tab-selected', {
|
|
5
|
+
small: 'scale(1.1)',
|
|
6
|
+
medium: 'scale(1.2)',
|
|
7
|
+
large: 'scale(1.3)',
|
|
8
|
+
xlarge: 'scale(1.3, 1.5)',
|
|
9
|
+
});
|
|
197
10
|
};
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
clonedEl.style.setProperty('display', 'none');
|
|
206
|
-
document.body.appendChild(clonedEl);
|
|
207
|
-
return clonedEl;
|
|
11
|
+
export const registerSegmentEffect = (targetElement) => {
|
|
12
|
+
return registerEffect(targetElement, 'ion-segment-button', 'segment-button-checked', {
|
|
13
|
+
small: 'scale(1.35)',
|
|
14
|
+
medium: 'scale(1.45)',
|
|
15
|
+
large: 'scale(1.55)',
|
|
16
|
+
xlarge: 'scale(1.55, 1.65)',
|
|
17
|
+
});
|
|
208
18
|
};
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY,GAAI,SAAS,MAAM,KAAG,WAY9C,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,gBAAgB,MAAM,EAAE,cAAc,MAAM,EAAE,mBAAmB,OAAO,KAAG,MAavG,CAAC"}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const cloneElement = (tagName) => {
|
|
2
|
+
const getCachedEl = document.querySelector(`${tagName}.ion-cloned-element`);
|
|
3
|
+
if (getCachedEl !== null) {
|
|
4
|
+
return getCachedEl;
|
|
5
|
+
}
|
|
6
|
+
const clonedEl = document.createElement(tagName);
|
|
7
|
+
clonedEl.classList.add('ion-cloned-element');
|
|
8
|
+
clonedEl.style.setProperty('display', 'none');
|
|
9
|
+
document.body.appendChild(clonedEl);
|
|
10
|
+
return clonedEl;
|
|
11
|
+
};
|
|
12
|
+
export const getTransform = (detailCurrentX, tabEffectElY, tabSelectedActual) => {
|
|
13
|
+
const diff = -2;
|
|
14
|
+
const currentX = detailCurrentX - tabSelectedActual.clientWidth / 2;
|
|
15
|
+
const maxLeft = tabSelectedActual.getBoundingClientRect().left + diff;
|
|
16
|
+
const maxRight = tabSelectedActual.getBoundingClientRect().right - diff - tabSelectedActual.clientWidth;
|
|
17
|
+
if (maxLeft < currentX && currentX < maxRight) {
|
|
18
|
+
return `translate3d(${currentX}px, ${tabEffectElY}px, 0)`;
|
|
19
|
+
}
|
|
20
|
+
if (maxLeft > currentX) {
|
|
21
|
+
return `translate3d(${maxLeft}px, ${tabEffectElY}px, 0)`;
|
|
22
|
+
}
|
|
23
|
+
return `translate3d(${maxRight}px, ${tabEffectElY}px, 0)`;
|
|
24
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rdlabo/ionic-theme-ios26",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "iOS26 CSS Theme for Ionic Framework",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "npm run build:css && npm run build:ts",
|
|
22
|
-
"build:css": "rm -rf dist/css && sass src:dist/css --style=compressed --no-source-map",
|
|
22
|
+
"build:css": "rm -rf dist/css && sass src/styles:dist/css --style=compressed --no-source-map",
|
|
23
23
|
"build:ts": "tsc",
|
|
24
24
|
"build:demo": "npm run build && cd demo && npm install && npm run build -- --configuration=production",
|
|
25
25
|
"prepare": "husky install",
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { EffectScales, registeredEffect } from './interfaces';
|
|
2
|
+
import { createGesture, GestureDetail, createAnimation } from '@ionic/core';
|
|
3
|
+
import type { Animation } from '@ionic/core/dist/types/utils/animation/animation-interface';
|
|
4
|
+
import { Gesture } from '@ionic/core/dist/types/utils/gesture';
|
|
5
|
+
import { cloneElement, getTransform } from './utils';
|
|
6
|
+
|
|
7
|
+
const GESTURE_NAME = 'ios26-enable-gesture';
|
|
8
|
+
const ANIMATED_NAME = 'ios26-animated';
|
|
9
|
+
|
|
10
|
+
export const registerEffect = (
|
|
11
|
+
targetElement: HTMLElement,
|
|
12
|
+
effectTagName: string,
|
|
13
|
+
selectedClassName: string,
|
|
14
|
+
scales: EffectScales,
|
|
15
|
+
): registeredEffect | undefined => {
|
|
16
|
+
if (!targetElement.classList.contains('ios')) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let gesture!: Gesture;
|
|
21
|
+
let moveAnimation: Animation | undefined;
|
|
22
|
+
let currentTouchedElement: HTMLElement | undefined;
|
|
23
|
+
let animationLatestX: number | undefined;
|
|
24
|
+
let effectElementPositionY: number | undefined;
|
|
25
|
+
|
|
26
|
+
let enterAnimationPromise: Promise<void> | undefined;
|
|
27
|
+
let moveAnimationPromise: Promise<void> | undefined;
|
|
28
|
+
let clearActivatedTimer: ReturnType<typeof setTimeout> | undefined;
|
|
29
|
+
|
|
30
|
+
const effectElement = cloneElement(effectTagName);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* These event listeners fix a bug where gestures don't complete properly.
|
|
34
|
+
* They terminate the gesture using native events as a fallback.
|
|
35
|
+
*/
|
|
36
|
+
const onPointerDown = () => {
|
|
37
|
+
clearActivated();
|
|
38
|
+
currentTouchedElement?.classList.remove('ion-activated');
|
|
39
|
+
gesture.destroy();
|
|
40
|
+
createAnimationGesture();
|
|
41
|
+
};
|
|
42
|
+
const onPointerUp = (event: PointerEvent) => {
|
|
43
|
+
clearActivatedTimer = setTimeout(() => {
|
|
44
|
+
onEndGesture();
|
|
45
|
+
currentTouchedElement?.classList.remove('ion-activated');
|
|
46
|
+
gesture.destroy();
|
|
47
|
+
createAnimationGesture();
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
targetElement.addEventListener('pointerdown', onPointerDown);
|
|
52
|
+
targetElement.addEventListener('pointerup', onPointerUp);
|
|
53
|
+
|
|
54
|
+
const createAnimationGesture = () => {
|
|
55
|
+
targetElement.classList.add(GESTURE_NAME);
|
|
56
|
+
gesture = createGesture({
|
|
57
|
+
el: targetElement,
|
|
58
|
+
threshold: 0,
|
|
59
|
+
gestureName: `${GESTURE_NAME}_${effectTagName}_${crypto.randomUUID()}`,
|
|
60
|
+
onStart: (event) => onStartGesture(event),
|
|
61
|
+
onMove: (event) => onMoveGesture(event),
|
|
62
|
+
onEnd: (event) => onEndGesture(),
|
|
63
|
+
});
|
|
64
|
+
gesture.enable(true);
|
|
65
|
+
};
|
|
66
|
+
createAnimationGesture();
|
|
67
|
+
|
|
68
|
+
const clearActivated = () => {
|
|
69
|
+
if (!currentTouchedElement) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
requestAnimationFrame(() => {
|
|
73
|
+
effectElement.style.display = 'none';
|
|
74
|
+
effectElement.innerHTML = '';
|
|
75
|
+
effectElement.style.transform = 'none';
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
targetElement.classList.remove(ANIMATED_NAME);
|
|
79
|
+
currentTouchedElement = undefined;
|
|
80
|
+
moveAnimation = undefined; // 次回のために破棄
|
|
81
|
+
moveAnimationPromise = undefined;
|
|
82
|
+
enterAnimationPromise = undefined; // 次回のためにリセット
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const onStartGesture = (detail: GestureDetail): boolean | undefined => {
|
|
86
|
+
enterAnimationPromise = undefined;
|
|
87
|
+
currentTouchedElement = ((detail.event.target as HTMLElement).closest(effectTagName) as HTMLElement) || undefined;
|
|
88
|
+
const tabSelectedElement = targetElement.querySelector(`${effectTagName}.${selectedClassName}`);
|
|
89
|
+
if (currentTouchedElement === undefined || tabSelectedElement === null) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
effectElementPositionY = tabSelectedElement.getBoundingClientRect().top;
|
|
93
|
+
|
|
94
|
+
const startTransform = getTransform(
|
|
95
|
+
tabSelectedElement.getBoundingClientRect().left + tabSelectedElement.clientWidth / 2,
|
|
96
|
+
effectElementPositionY,
|
|
97
|
+
tabSelectedElement,
|
|
98
|
+
);
|
|
99
|
+
const middleTransform = getTransform(
|
|
100
|
+
(tabSelectedElement.getBoundingClientRect().left + tabSelectedElement.clientWidth / 2 + detail.currentX) / 2,
|
|
101
|
+
effectElementPositionY,
|
|
102
|
+
currentTouchedElement,
|
|
103
|
+
);
|
|
104
|
+
const endTransform = getTransform(detail.currentX, effectElementPositionY, currentTouchedElement);
|
|
105
|
+
const enterAnimation = createAnimation();
|
|
106
|
+
enterAnimation
|
|
107
|
+
.addElement(effectElement)
|
|
108
|
+
.delay(70)
|
|
109
|
+
.beforeStyles({
|
|
110
|
+
width: `${tabSelectedElement.clientWidth}px`,
|
|
111
|
+
height: `${tabSelectedElement.clientHeight}px`,
|
|
112
|
+
display: 'block',
|
|
113
|
+
})
|
|
114
|
+
.beforeAddWrite(() => {
|
|
115
|
+
tabSelectedElement.childNodes.forEach((node) => {
|
|
116
|
+
effectElement.appendChild(node.cloneNode(true));
|
|
117
|
+
});
|
|
118
|
+
targetElement.classList.add(ANIMATED_NAME);
|
|
119
|
+
currentTouchedElement!.classList.add('ion-activated');
|
|
120
|
+
currentTouchedElement!.click();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (currentTouchedElement === tabSelectedElement) {
|
|
124
|
+
enterAnimation
|
|
125
|
+
.keyframes([
|
|
126
|
+
{
|
|
127
|
+
transform: `${startTransform} ${scales.small}`,
|
|
128
|
+
opacity: 1,
|
|
129
|
+
offset: 0,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
transform: `${middleTransform} ${scales.large}`,
|
|
133
|
+
opacity: 1,
|
|
134
|
+
offset: 0.6,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
transform: `${endTransform} ${scales.medium}`,
|
|
138
|
+
opacity: 1,
|
|
139
|
+
offset: 1,
|
|
140
|
+
},
|
|
141
|
+
])
|
|
142
|
+
.duration(160);
|
|
143
|
+
} else {
|
|
144
|
+
enterAnimation
|
|
145
|
+
.keyframes([
|
|
146
|
+
{
|
|
147
|
+
transform: `${startTransform} ${scales.small}`,
|
|
148
|
+
opacity: 1,
|
|
149
|
+
offset: 0,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
transform: `${middleTransform} ${scales.large}`,
|
|
153
|
+
opacity: 1,
|
|
154
|
+
offset: 0.65,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
transform: `${endTransform} ${scales.medium}`,
|
|
158
|
+
opacity: 1,
|
|
159
|
+
offset: 1,
|
|
160
|
+
},
|
|
161
|
+
])
|
|
162
|
+
.duration(280);
|
|
163
|
+
}
|
|
164
|
+
animationLatestX = detail.currentX;
|
|
165
|
+
enterAnimationPromise = enterAnimation.play().then(() => {
|
|
166
|
+
enterAnimationPromise = undefined;
|
|
167
|
+
});
|
|
168
|
+
return true;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const onMoveGesture = (detail: GestureDetail): boolean | undefined => {
|
|
172
|
+
if (currentTouchedElement === undefined || enterAnimationPromise || moveAnimationPromise) {
|
|
173
|
+
return true; // Skip Animation
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const startTransform = getTransform(animationLatestX!, effectElementPositionY!, currentTouchedElement);
|
|
177
|
+
const endTransform = getTransform(detail.currentX, effectElementPositionY!, currentTouchedElement);
|
|
178
|
+
|
|
179
|
+
// Move用のアニメーションオブジェクトを初回のみ作成し、再利用する
|
|
180
|
+
if (!moveAnimation) {
|
|
181
|
+
moveAnimation = createAnimation();
|
|
182
|
+
moveAnimation
|
|
183
|
+
.addElement(effectElement)
|
|
184
|
+
.duration(800)
|
|
185
|
+
.easing('ease-in-out')
|
|
186
|
+
.keyframes([
|
|
187
|
+
{
|
|
188
|
+
transform: `${startTransform} ${scales.medium}`,
|
|
189
|
+
opacity: 1,
|
|
190
|
+
offset: 0,
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
transform: `${startTransform} ${scales.xlarge}`,
|
|
194
|
+
opacity: 1,
|
|
195
|
+
offset: 0.2,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
transform: `${endTransform} ${scales.medium}`,
|
|
199
|
+
opacity: 1,
|
|
200
|
+
offset: 1,
|
|
201
|
+
},
|
|
202
|
+
]);
|
|
203
|
+
} else {
|
|
204
|
+
moveAnimation.duration(0).keyframes([
|
|
205
|
+
{
|
|
206
|
+
transform: `${endTransform} ${scales.medium}`,
|
|
207
|
+
opacity: 1,
|
|
208
|
+
offset: 1,
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
transform: `${endTransform} ${scales.medium}`,
|
|
212
|
+
opacity: 1,
|
|
213
|
+
offset: 1,
|
|
214
|
+
},
|
|
215
|
+
]);
|
|
216
|
+
}
|
|
217
|
+
animationLatestX = detail.currentX;
|
|
218
|
+
moveAnimationPromise = moveAnimation.play().then(() => {
|
|
219
|
+
moveAnimationPromise = undefined;
|
|
220
|
+
});
|
|
221
|
+
return true;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const onEndGesture = (): boolean | undefined => {
|
|
225
|
+
// タイマーをクリア(正常にonEndGestureが実行された場合)
|
|
226
|
+
if (clearActivatedTimer !== undefined) {
|
|
227
|
+
clearTimeout(clearActivatedTimer);
|
|
228
|
+
clearActivatedTimer = undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (currentTouchedElement === undefined) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const transform = getTransform(animationLatestX!, effectElementPositionY!, currentTouchedElement);
|
|
236
|
+
|
|
237
|
+
const leaveAnimation = createAnimation();
|
|
238
|
+
leaveAnimation.addElement(effectElement);
|
|
239
|
+
leaveAnimation
|
|
240
|
+
.onFinish(() => clearActivated())
|
|
241
|
+
.easing('ease-in')
|
|
242
|
+
.duration(80)
|
|
243
|
+
.keyframes([
|
|
244
|
+
{
|
|
245
|
+
transform: `${transform} ${scales.medium}`,
|
|
246
|
+
opacity: 1,
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
transform: `${transform} ${scales.small}`,
|
|
250
|
+
opacity: 0,
|
|
251
|
+
},
|
|
252
|
+
]);
|
|
253
|
+
(async () => {
|
|
254
|
+
// Wait for enter animation to complete before playing leave animation
|
|
255
|
+
if (enterAnimationPromise) {
|
|
256
|
+
setTimeout(() => currentTouchedElement!.classList.remove('ion-activated'), 50);
|
|
257
|
+
await enterAnimationPromise;
|
|
258
|
+
} else {
|
|
259
|
+
currentTouchedElement!.classList.remove('ion-activated');
|
|
260
|
+
}
|
|
261
|
+
leaveAnimation.play();
|
|
262
|
+
})();
|
|
263
|
+
return true;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
destroy: () => {
|
|
268
|
+
// Remove event listeners
|
|
269
|
+
targetElement.removeEventListener('pointerdown', onPointerDown);
|
|
270
|
+
targetElement.removeEventListener('pointerup', onPointerUp);
|
|
271
|
+
|
|
272
|
+
// Clear any pending timer
|
|
273
|
+
if (clearActivatedTimer !== undefined) {
|
|
274
|
+
clearTimeout(clearActivatedTimer);
|
|
275
|
+
clearActivatedTimer = undefined;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Clear activated state
|
|
279
|
+
clearActivated();
|
|
280
|
+
|
|
281
|
+
// Destroy gesture
|
|
282
|
+
if (gesture) {
|
|
283
|
+
gesture.destroy();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Remove gesture class
|
|
287
|
+
targetElement.classList.remove(GESTURE_NAME);
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const cloneElement = (tagName: string): HTMLElement => {
|
|
2
|
+
const getCachedEl = document.querySelector(`${tagName}.ion-cloned-element`);
|
|
3
|
+
if (getCachedEl !== null) {
|
|
4
|
+
return getCachedEl as HTMLElement;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const clonedEl = document.createElement(tagName) as HTMLElement;
|
|
8
|
+
clonedEl.classList.add('ion-cloned-element');
|
|
9
|
+
clonedEl.style.setProperty('display', 'none');
|
|
10
|
+
document.body.appendChild(clonedEl);
|
|
11
|
+
|
|
12
|
+
return clonedEl;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const getTransform = (detailCurrentX: number, tabEffectElY: number, tabSelectedActual: Element): string => {
|
|
16
|
+
const diff = -2;
|
|
17
|
+
const currentX = detailCurrentX - tabSelectedActual.clientWidth / 2;
|
|
18
|
+
const maxLeft = tabSelectedActual.getBoundingClientRect().left + diff;
|
|
19
|
+
const maxRight = tabSelectedActual.getBoundingClientRect().right - diff - tabSelectedActual.clientWidth;
|
|
20
|
+
|
|
21
|
+
if (maxLeft < currentX && currentX < maxRight) {
|
|
22
|
+
return `translate3d(${currentX}px, ${tabEffectElY}px, 0)`;
|
|
23
|
+
}
|
|
24
|
+
if (maxLeft > currentX) {
|
|
25
|
+
return `translate3d(${maxLeft}px, ${tabEffectElY}px, 0)`;
|
|
26
|
+
}
|
|
27
|
+
return `translate3d(${maxRight}px, ${tabEffectElY}px, 0)`;
|
|
28
|
+
};
|