Skip to content

Commit 5e04012

Browse files
committed
refactor: update project structure
1 parent 5f56094 commit 5e04012

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+309
-306
lines changed

demo/src/global.scss

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@
3636
//@use '@ionic/angular/css/palettes/dark.system.css';
3737
@use '@ionic/angular/css/palettes/dark.class.css';
3838

39-
@use '../../src/default-variables';
40-
@use '../../src/ionic-theme-ios26';
41-
@use '../../src/ionic-theme-ios26-dark-class';
42-
@use '../../src/md-ion-list-inset';
43-
@use '../../src/md-remove-ios-class-effect';
39+
@use '../../src/styles/default-variables';
40+
@use '../../src/styles/ionic-theme-ios26';
41+
@use '../../src/styles/ionic-theme-ios26-dark-class';
42+
@use '../../src/styles/md-ion-list-inset';
43+
@use '../../src/styles/md-remove-ios-class-effect';
4444

4545
:root {
4646
--ios26-content-box-shadow-rgb: var(--ion-color-light-rgb);

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
],
2020
"scripts": {
2121
"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",
2323
"build:ts": "tsc",
2424
"build:demo": "npm run build && cd demo && npm install && npm run build -- --configuration=production",
2525
"prepare": "husky install",

src/gestures/index.ts

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
};

src/gestures/interfaces.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export interface EffectScales {
2+
small: string;
3+
medium: string;
4+
large: string;
5+
xlarge: string;
6+
}
7+
8+
export interface registeredEffect {
9+
destroy: () => void;
10+
}
File renamed without changes.

0 commit comments

Comments
 (0)