diff --git a/.claude/skills/code-optimize/SKILL.md b/.claude/skills/code-optimize/SKILL.md
new file mode 100644
index 0000000..18a6804
--- /dev/null
+++ b/.claude/skills/code-optimize/SKILL.md
@@ -0,0 +1,454 @@
+---
+name: code-optimize
+description: 代碼優化規範。當需要重構、優化或審查代碼品質時使用此 skill。確保代碼符合單一職責、可讀性與維護性。
+metadata:
+ version: "2.0"
+ framework: reactjs
+---
+
+# 代碼優化規範
+
+## 1. 註解規範
+
+所有 props interface 屬性、自訂 hooks、工具函式必須加上 JSDoc 註解。
+
+### Props 註解
+```typescript
+interface IProps {
+ /** 是否展開選單 */
+ isExpand?: boolean
+ /** 選單項目列表 */
+ items: MenuItem[]
+ /** 點擊項目時觸發 */
+ onItemClick?: (item: MenuItem) => void
+}
+```
+
+### 函式註解(必須包含 @param 和 @returns)
+
+```typescript
+// ✅ 正確:完整的函式註解
+/**
+ * 切換選單狀態
+ * @param status - 目標狀態
+ * @returns 是否切換成功
+ */
+const toggleMenu = (status: boolean): boolean => {
+ // ...
+};
+
+/**
+ * 取得可見項目
+ * @returns 過濾後的可見項目陣列
+ */
+const getVisibleItems = (): MenuItem[] => {
+ return items.filter(item => item.isVisible);
+};
+
+/**
+ * 計算項目總價
+ * @param items - 項目陣列
+ * @param discount - 折扣比例(0-1)
+ * @returns 計算後的總價
+ */
+const calculateTotal = (items: Item[], discount: number): number => {
+ return items.reduce((sum, item) => sum + item.price, 0) * (1 - discount);
+};
+
+// ❌ 錯誤:缺少 @param 和 @returns
+/**
+ * 切換選單狀態
+ */
+const toggleMenu = (status: boolean): boolean => {
+ // ...
+};
+
+// ❌ 錯誤:完全沒有註解
+const toggleMenu = (status: boolean): boolean => {
+ // ...
+};
+```
+
+### 元件註解
+
+```typescript
+/**
+ * Component / Molecules @ 選單元件
+ * @param isExpand - 是否展開
+ * @param items - 選單項目
+ */
+const Menu = ({
+ isExpand = true,
+ items,
+}: IProps) => {
+ // ...
+};
+```
+
+### 無參數或 void 回傳
+
+```typescript
+// 無參數時省略 @param,void 回傳時省略 @returns
+/**
+ * 重置選單狀態
+ */
+const reset = (): void => {
+ setIsExpand(false);
+};
+
+// 有參數但無回傳
+/**
+ * 更新選單位置
+ * @param offset - 偏移量(像素)
+ */
+const updatePosition = (offset: number): void => {
+ setPosition(offset);
+};
+```
+
+## 2. 早期返回模式 (Early Return)
+
+避免 `if-else` 巢狀,改用早期返回提升可讀性:
+
+```typescript
+// ❌ 錯誤:if-else 巢狀
+const updatePosition = () => {
+ if (containerRef.current) {
+ if (isExpand) {
+ const target = findTarget();
+ if (target) {
+ setOffset(target.offsetTop);
+ } else {
+ setOffset(0);
+ }
+ } else {
+ setOffset(0);
+ }
+ } else {
+ setOffset(0);
+ }
+};
+
+// ✅ 正確:早期返回
+const updatePosition = () => {
+ if (!containerRef.current) {
+ setOffset(0);
+ return;
+ }
+
+ if (!isExpand) {
+ setOffset(0);
+ return;
+ }
+
+ const target = findTarget();
+ if (!target) {
+ setOffset(0);
+ return;
+ }
+
+ setOffset(target.offsetTop);
+};
+```
+
+## 3. 函數式陣列操作
+
+避免 `push`,優先使用 `map`、`filter`、`reduce` 等函數式方法:
+
+```typescript
+// ❌ 錯誤:使用 push
+const getVisibleItems = (): Item[] => {
+ const result: Item[] = [];
+ for (const item of items) {
+ if (item.isVisible) {
+ result.push({
+ ...item,
+ label: item.name.toUpperCase()
+ });
+ }
+ }
+ return result;
+};
+
+// ✅ 正確:函數式寫法
+const visibleItems = useMemo(() => {
+ return items
+ .filter(item => item.isVisible)
+ .map(item => ({
+ ...item,
+ label: item.name.toUpperCase()
+ }));
+}, [items]);
+
+// ✅ 正確:需要累加時使用 reduce
+const totalPrice = useMemo(() => {
+ return items.reduce((sum, item) => sum + item.price, 0);
+}, [items]);
+```
+
+## 4. 檔案分離原則
+
+元件、型別、樣式、資料分離至獨立檔案:
+
+```
+ComponentName/
+├── ComponentName.tsx # 元件邏輯與 JSX
+├── types.ts # 型別定義 (interface / type)
+├── data.ts # 假資料或常數
+└── index.ts # 匯出入口
+```
+
+```typescript
+// ❌ 錯誤:型別與元件寫在一起、inline styles
+const Menu = ({items}: {items: MenuItem[]}) => {
+ return
...
;
+};
+
+// ✅ 正確:分離型別,使用 styled-components
+// types.ts
+interface IProps {
+ /** 選單項目 */
+ items: MenuItem[]
+}
+
+// Menu.tsx
+import {IProps} from './types';
+
+const Menu = ({items}: IProps) => {
+ return ...;
+};
+
+const MenuRoot = styled.div`
+ padding: 16px;
+`;
+```
+
+## 5. 棄用標記
+
+未被使用的代碼必須標記 `@deprecated` 並說明替代方案:
+
+```typescript
+/**
+ * @deprecated 請改用 `useNewHook()`,將於 v2.0 移除
+ */
+const useOldHook = () => {
+ console.warn('useOldHook is deprecated, use useNewHook instead');
+ return useNewHook();
+};
+
+/**
+ * @deprecated 請改用 `isExpand` prop
+ */
+interface IProps {
+ /** @deprecated 請改用 isExpand */
+ expanded?: boolean
+ isExpand?: boolean
+}
+```
+
+## 6. 單一職責原則 (SRP)
+
+每個元件、hook、函式只負責一件事:
+
+### 元件職責分離
+
+```typescript
+// ❌ 錯誤:混合多個職責
+const Menu = ({items}: IProps) => {
+ // 職責1: UI 渲染
+ const [isOpen, setIsOpen] = useState(false);
+
+ // 職責2: 拖動邏輯
+ const [isDragging, setIsDragging] = useState(false);
+ const onDragStart = () => { /* ... */ };
+ const onDragMove = () => { /* ... */ };
+ const onDragEnd = () => { /* ... */ };
+
+ // 職責3: 通知管理
+ const [notifications, setNotifications] = useState([]);
+ const showNotification = () => { /* ... */ };
+
+ return ...
;
+};
+
+// ✅ 正確:分離職責
+// 拖動邏輯抽離至自訂 hook
+const useDraggable = () => {
+ const [isDragging, setIsDragging] = useState(false);
+ const onDragStart = useCallback(() => { /* ... */ }, []);
+ const onDragMove = useCallback(() => { /* ... */ }, []);
+ const onDragEnd = useCallback(() => { /* ... */ }, []);
+ return {isDragging, onDragStart, onDragMove, onDragEnd};
+};
+
+// 元件只負責 UI 渲染
+const Menu = ({items, isDragging, notification}: IProps) => {
+ const [isOpen, setIsOpen] = useState(false);
+ return ...;
+};
+```
+
+### 控制邏輯由父元件管理
+
+子元件只負責「做一件事」,控制邏輯由父元件管理:
+
+```typescript
+// ❌ 錯誤:子元件自己管理循序播放邏輯
+const IconButton = ({animationDelay, animationCycleDuration}: IProps) => {
+ useEffect(() => {
+ const timer = setTimeout(() => play(), animationCycleDuration);
+ return () => clearTimeout(timer);
+ }, [animationCycleDuration]);
+ // ...
+};
+
+// ✅ 正確:子元件只負責播放,父元件控制順序
+const IconButton = ({isPlaying, onAnimationComplete}: IProps) => {
+ // 子元件職責單純:播放動畫、發出完成事件
+ return ;
+};
+
+const Menu = ({items}: IProps) => {
+ const [currentPlayingIndex, setCurrentPlayingIndex] = useState(0);
+
+ const handleAnimationComplete = useCallback((index: number) => {
+ setCurrentPlayingIndex((index + 1) % items.length);
+ }, [items.length]);
+
+ return <>
+ {items.map((item, index) => (
+ handleAnimationComplete(index)}
+ />
+ ))}
+ >;
+};
+```
+
+## 7. 方法長度限制
+
+單一函式不超過 20 行,超過則拆分:
+
+```typescript
+// ❌ 錯誤:函式過長
+const processData = () => {
+ // 驗證
+ if (!data) return;
+ if (!data.items) return;
+
+ // 過濾
+ const filtered = data.items.filter(/* ... */);
+
+ // 轉換
+ const transformed = filtered.map(/* ... */);
+
+ // 排序
+ const sorted = transformed.sort(/* ... */);
+
+ // 更新
+ setResult(sorted);
+ updateUI();
+ notifyParent();
+};
+
+// ✅ 正確:拆分為小函式
+const isValidData = (data: Data | null): data is Data => {
+ return !!data?.items;
+};
+
+const transformData = (items: Item[]): Item[] => {
+ return items
+ .filter(item => item.isActive)
+ .map(item => ({...item, processed: true}))
+ .sort((a, b) => a.order - b.order);
+};
+
+const processData = () => {
+ if (!isValidData(data)) return;
+
+ setResult(transformData(data.items));
+ updateUI();
+ notifyParent();
+};
+```
+
+## 8. 命名規範
+
+```typescript
+// 布林值使用 is/has/can/should 前綴
+const [isExpand, setIsExpand] = useState(false);
+const [hasNotification, setHasNotification] = useState(false);
+const [canEdit, setCanEdit] = useState(true);
+
+// 事件處理使用 handle 前綴,回呼 prop 使用 on 前綴
+interface IProps {
+ onItemClick?: (item: Item) => void
+ onToggle?: (isOpen: boolean) => void
+}
+
+const handleItemClick = (item: Item) => {
+ onItemClick?.(item);
+};
+
+// 自訂 hook 使用 use 前綴
+const useMenuState = () => { /* ... */ };
+const useDraggable = () => { /* ... */ };
+```
+
+## 9. 禁止使用 any
+
+如果是 global 方法,例如 window 下的自定義,需定義在 src/types/global.d.ts 中
+
+其他情況需提供型別,如果是自己使用的,放在自己歸屬的路徑下
+
+如果不屬於誰的,屬於專案本身,則放在共用的地方
+
+
+## 10. 效能考慮
+
+優先順序為閱讀性,次要為效能考慮,避免造成頻繁渲染的寫法:
+
+```typescript
+// ❌ 錯誤:每次渲染都建立新物件/函式
+const Menu = ({items}: IProps) => {
+ return items.map(item => (
+ - handleClick(item.id)}
+ />
+ ));
+};
+
+// ✅ 正確:使用 useMemo / useCallback 避免不必要的重渲染
+const Menu = ({items}: IProps) => {
+ const handleClick = useCallback((id: string) => {
+ // ...
+ }, []);
+
+ return items.map(item => (
+
+ ));
+};
+```
+
+## 快速檢查清單
+
+優化代碼時,逐項確認:
+
+- [ ] 所有 props interface 屬性、自訂 hooks、工具函式都有 JSDoc 註解
+- [ ] 沒有超過 2 層的 `if-else` 巢狀
+- [ ] 沒有使用 `array.push()`,改用 `map`/`filter`/`reduce`
+- [ ] 型別、元件、樣式分離至獨立檔案
+- [ ] 未使用的代碼標記 `@deprecated`
+- [ ] 每個元件/hook/函式只有單一職責
+- [ ] 函式不超過 20 行
+- [ ] 命名符合規範(is/has/can、handle/on、use)
+- [ ] 型別規範(禁止 any)
+- [ ] 效能考慮(useMemo / useCallback)
diff --git a/.claude/skills/code-review/SKILL.md b/.claude/skills/code-review/SKILL.md
new file mode 100644
index 0000000..6c47f05
--- /dev/null
+++ b/.claude/skills/code-review/SKILL.md
@@ -0,0 +1,71 @@
+---
+name: code-review
+description: Code Review 工具。分析 git diff 中的變更,依據專案規範提出審查意見,完成分析後需二次確認才執行修改。
+user_invocable: true
+metadata:
+ version: "2.0"
+ framework: reactjs
+---
+
+# Code Review
+
+## 流程
+
+### 步驟 1:收集變更
+
+執行 `git diff` 取得當前所有未暫存的變更。若使用者有指定檔案或範圍,則只分析該範圍。
+
+### 步驟 2:逐檔分析
+
+針對每個變更檔案,檢查以下項目:
+
+#### 正確性
+- 邏輯是否正確,有無遺漏的邊界條件
+- 是否有殘留的 dead code、未使用的 import、未清理的註解
+- hooks 相依陣列是否正確(useEffect、useMemo、useCallback 的 deps)
+- useEffect 是否有正確的 cleanup function(清除 timer、取消訂閱、移除 event listener)
+- 元件是否有 key prop 問題(列表渲染缺少 key 或使用 index 作為 key)
+
+#### 一致性
+- 命名是否符合專案慣例(boolean 用 is/has/can 前綴、事件回呼 prop 用 on 前綴、handler 用 handle 前綴、hook 用 use 前綴)
+- 是否與周邊程式碼風格一致
+- 型別定義是否放在正確位置(types.ts)
+
+#### 安全性
+- 是否有潛在的記憶體洩漏(未清理的 subscription、timer、event listener)
+- 是否有不安全的型別轉換(as any、型別斷言)
+- 是否有 XSS 風險(dangerouslySetInnerHTML)
+
+#### 簡潔性
+- 是否有可以簡化的邏輯
+- 是否有不必要的中間變數
+- 是否有可以用 useMemo/useCallback 優化的部分
+
+### 步驟 3:輸出審查報告
+
+使用以下格式輸出:
+
+```
+## Code Review 報告
+
+### [檔案路徑]
+
+**[問題等級]** 問題描述
+- 位置:L行號
+- 建議:具體修改建議
+
+---
+問題等級:
+- MUST FIX — 必須修正(邏輯錯誤、記憶體洩漏、安全問題)
+- SUGGESTION — 建議改善(可讀性、一致性、簡潔性)
+- NITPICK — 小建議(命名、格式)
+- GOOD — 值得肯定的改動
+```
+
+### 步驟 4:二次確認
+
+報告輸出後,使用 AskUserQuestion 工具詢問使用者:
+- 哪些項目要執行修改
+- 哪些項目忽略
+
+**在使用者明確回覆前,不得進行任何程式碼修改。**
diff --git a/src/manager/Dragger/Dragger.ts b/src/manager/Dragger/Dragger.ts
index 3c0561b..d0fb43b 100644
--- a/src/manager/Dragger/Dragger.ts
+++ b/src/manager/Dragger/Dragger.ts
@@ -14,8 +14,9 @@ import {calcMoveTranslatePx} from './utils';
/**
- * 托動控制器
- * unmount 跟 blur 都需要 停止計時器
+ * 拖動控制器
+ * 負責處理桌面 pointer 與手機 touch 事件的拖曳邏輯
+ * unmount 跟 blur 都需要停止計時器
*/
class Dragger {
private _configurator: Configurator;
@@ -24,7 +25,11 @@ class Dragger {
private _locator: Locator;
private _stater: Stater;
private _eventor = new Eventor();
- private _moveMinDistancePx = 50; // 最少滑動多少距離Px才檢查比例切換上下頁
+
+ /** 一次性消費旗標,拖曳開始時設 true,click capture 消費時設 false */
+ private _isDragged = false;
+ /** 拖曳偵測的最小距離(等同 cdkDrag 內建閾值),低於此值視為點擊而非拖曳 */
+ private _dragThresholdPx = 5;
constructor(manager: {
configurator: Configurator,
@@ -40,50 +45,82 @@ class Dragger {
}
+ /**
+ * 註冊拖曳開始事件與 capture click 攔截器
+ * @param callBack - 拖曳開始時的回呼
+ */
onDragStart = (callBack?: TEventMap['dragStart']) => {
if(this._elementor.containerEl){
if(checkIsMobile()){
this._elementor.containerEl.addEventListener('touchstart', this._onMobileTouchStart, {passive: false});
}else{
- this._elementor.containerEl.addEventListener('pointerdown', this._onWebMouseStart, {passive: false} as any);
+ this._elementor.containerEl.addEventListener('pointerdown', this._onWebMouseStart, {passive: false} as EventListenerOptions);
}
}
+ if(this._elementor.rootEl){
+ this._elementor.rootEl.addEventListener('click', this._onCaptureClick, true);
+ }
+
this._eventor.on('dragStart', callBack);
};
+ /**
+ * 註冊拖曳移動事件
+ * @param callBack - 拖曳移動時的回呼
+ */
onDragMove = (callBack?: TEventMap['dragMove']) => {
this._eventor.on('dragMove', callBack);
};
+ /**
+ * 註冊拖曳結束事件
+ * @param callBack - 拖曳結束時的回呼
+ */
onDragEnd = (callBack?: TEventMap['dragEnd']) => {
this._eventor.on('dragEnd', callBack);
};
+ /**
+ * 移除拖曳開始事件與 capture click 攔截器
+ * @param callBack - 要移除的回呼
+ */
offDragStart = (callBack?: TEventMap['dragStart']) => {
if(this._elementor.containerEl){
if(checkIsMobile()) {
- this._elementor.containerEl.removeEventListener('touchstart', this._onMobileTouchStart, {passive: false} as any);
+ this._elementor.containerEl.removeEventListener('touchstart', this._onMobileTouchStart, {passive: false} as EventListenerOptions);
}else{
- this._elementor.containerEl.removeEventListener('pointerdown', this._onWebMouseStart, {passive: false} as any);
+ this._elementor.containerEl.removeEventListener('pointerdown', this._onWebMouseStart, {passive: false} as EventListenerOptions);
}
}
+ if(this._elementor.rootEl){
+ this._elementor.rootEl.removeEventListener('click', this._onCaptureClick, true);
+ }
+
this._eventor.off('dragStart', callBack);
};
+ /**
+ * 移除拖曳移動事件
+ * @param callBack - 要移除的回呼
+ */
offDragMove = (callBack?: TEventMap['dragMove']) => {
this._eventor.off('dragMove', callBack);
};
+ /**
+ * 移除拖曳結束事件
+ * @param callBack - 要移除的回呼
+ */
offDragEnd = (callBack?: TEventMap['dragEnd']) => {
this._eventor.off('dragEnd', callBack);
};
/**
- * mobile phone finger press start
- * @param event
+ * 手機觸控開始
+ * @param event - TouchEvent
*/
private _onMobileTouchStart = (event: TouchEvent): void => {
if(this._configurator.setting.isDebug && logEnable.dragger.onMobileTouchStart) logger.printInText('[Dragger._onMobileTouchStart]');
@@ -92,21 +129,18 @@ class Dragger {
this._dragStart();
this._eventor.emit('dragStart');
const {containerEl} = this._elementor;
- if (containerEl) {
+ if (!containerEl) return;
- // 移動到開始位置 避免跳動
- this._locator.touchStart(new MobileTouchEvent(event), containerEl);
+ this._locator.touchStart(new MobileTouchEvent(event), containerEl);
- // 設定移動 與 結束事件
- document.addEventListener('touchmove', this._onMobileTouchMove, {passive: false});
- document.addEventListener('touchend', this._onMobileTouchEnd, {passive: false});
- }
+ document.addEventListener('touchmove', this._onMobileTouchMove, {passive: false});
+ document.addEventListener('touchend', this._onMobileTouchEnd, {passive: false});
};
/**
- * Mobile phone finger press and move
- * @param event
+ * 手機觸控移動
+ * @param event - TouchEvent
*/
private _onMobileTouchMove = (event: TouchEvent): void => {
event.stopPropagation();
@@ -125,20 +159,19 @@ class Dragger {
});
}
+ if(!this._elementor.containerEl || !this._locator._letItGo) return;
+ event.preventDefault(); // 開始滑動禁止捲動BodyScroll
- if(this._elementor.containerEl && this._locator._letItGo){
- event.preventDefault(); // 開始滑動禁止捲動BodyScroll
- const movePx = this._locator.touchMove(touchEvent, this._elementor.containerEl);
- this._dragMove(movePx.x);
- }
+ const movePx = this._locator.touchMove(touchEvent, this._elementor.containerEl);
+ this._dragMove(movePx.x);
};
/**
- * Mobile phone finger press to end
- * @param event
+ * 手機觸控結束
+ * @param event - TouchEvent
*
- * PS: Add event.preventDefault(); will affect the mobile phone click onClick event
+ * PS: 不可加 event.preventDefault(),會影響手機 click onClick 事件
*/
private _onMobileTouchEnd = (event: TouchEvent): void => {
if(this._configurator.setting.isDebug && logEnable.dragger.onMobileTouchEnd) logger.printInText('[Dragger._onMobileTouchEnd]');
@@ -154,12 +187,9 @@ class Dragger {
};
-
-
-
/**
- * Web mouse click
- * @param event
+ * 桌面滑鼠按下
+ * @param event - PointerEvent
*/
private _onWebMouseStart = (event: PointerEvent): void => {
event.preventDefault();
@@ -170,56 +200,42 @@ class Dragger {
this._dragStart();
this._eventor.emit('dragStart');
-
const {containerEl} = this._elementor;
- if (containerEl) {
-
- // 移動到開始位置 避免跳動
- this._locator.touchStart(new PointerTouchEvent(event), containerEl);
- const {startPosition} = this._locator;
- const translateX = calcMoveTranslatePx(startPosition.x, event.clientX);
- this._elState
- .transform({translateX});
-
- // 設定移動 與 結束事件
- document.addEventListener('pointermove', this._onWebMouseMove, false);
- document.addEventListener('pointerup', this._onWebMouseEnd, false);
- document.addEventListener('pointercancel', this._onWebMouseEnd, false);
- }
+ if (!containerEl) return;
+ this._locator.touchStart(new PointerTouchEvent(event), containerEl);
+ const {startPosition} = this._locator;
+ const translateX = calcMoveTranslatePx(startPosition.x, event.clientX);
+ this._elState
+ .transform({translateX});
+
+ document.addEventListener('pointermove', this._onWebMouseMove, false);
+ document.addEventListener('pointerup', this._onWebMouseEnd, false);
+ document.addEventListener('pointercancel', this._onWebMouseEnd, false);
};
/**
- * Web mouse movement
- * @param event
+ * 桌面滑鼠移動
+ * @param event - PointerEvent
*/
private _onWebMouseMove = (event: PointerEvent):void => {
event.preventDefault();
if(this._configurator.setting.isDebug && logEnable.dragger.onWebMouseMove) logger.printInText('[Dragger._onWebMouseMove]');
+ if(!this._elementor.containerEl) return;
- // 判斷距離是否影響 onClick (只有桌面做開關)
- const startEndMoveX = this._locator._endPosition.pageX - this._locator._startPosition.pageX;
- if(Math.abs(startEndMoveX) > this._moveMinDistancePx) {
- this._elState.setTouching(true);
- }
-
- if(this._elementor.containerEl){
- this._elementor.containerEl.setPointerCapture(event.pointerId);
- const movePx = this._locator.touchMove(new PointerTouchEvent(event), this._elementor.containerEl);
-
- this._dragMove(movePx.x);
- }
+ this._elementor.containerEl.setPointerCapture(event.pointerId);
+ const movePx = this._locator.touchMove(new PointerTouchEvent(event), this._elementor.containerEl);
+ this._dragMove(movePx.x);
};
/**
- * web mouse release
- * @param event
+ * 桌面滑鼠放開
+ * @param event - PointerEvent
*/
private _onWebMouseEnd = (event: PointerEvent):void => {
event.preventDefault();
- this._elState.setTouching(false); // 只有桌面做開關
if(this._configurator.setting.isDebug && logEnable.dragger.onWebMouseEnd) logger.printInText('[Dragger._onWebMouseEnd]');
@@ -230,38 +246,61 @@ class Dragger {
};
+ /**
+ * Capture 階段攔截 click 事件
+ * 當偵測到拖曳時,阻止 click 冒泡到子元素的 onClick
+ * 攔截後立即消費旗標,確保下一次 click 正常放行
+ * @param event - MouseEvent
+ */
+ private _onCaptureClick = (event: MouseEvent): void => {
+ if(!this._isDragged) return;
+ event.stopPropagation();
+ event.preventDefault();
+ this._isDragged = false;
+ };
+
+ /**
+ * 拖曳開始,輸出除錯訊息
+ */
private _dragStart() {
if(this._configurator.setting.isDebug && logEnable.dragger.onDragStart) logger.printInText('[Dragger._dragStart]');
-
}
+ /**
+ * 處理拖曳移動中的位移與效果計算
+ * @param moveX - 水平移動距離(px)
+ */
private _dragMove(moveX: number) {
if(this._configurator.setting.isDebug && logEnable.dragger.onDragMove) logger.printInText('[Dragger._dragMove]');
const {setting} = this._configurator;
- if (this._elementor.containerEl &&
- setting.isEnableMouseMove &&
- this._elementor.slideItemEls &&
- this._stater.page.total > 1
- ) {
- const percentage = this._elState.getMovePercentage(moveX); //TODO: 應該移動到 Positioner
+ if (!this._elementor.containerEl ||
+ !setting.isEnableMouseMove ||
+ !this._elementor.slideItemEls ||
+ this._stater.page.total <= 1
+ ) return;
- // 同步控制
- this._eventor.emit('dragMove', percentage);
+ // ⭐ 等同 cdkDrag 內建閾值:低於最小距離不算拖曳,避免手指點擊微移誤判
+ if(Math.abs(moveX) < this._dragThresholdPx) return;
- this._elState
- .transform({translateX: moveX})
- .moveEffect(percentage);
- }
- }
+ // ⭐ 等同指南的 onDragStarted():確認拖曳發生,設定一次性消費旗標
+ this._isDragged = true;
+
+ const percentage = this._elState.getMovePercentage(moveX); //TODO: 應該移動到 Positioner
+ this._eventor.emit('dragMove', percentage);
+
+ this._elState
+ .transform({translateX: moveX})
+ .moveEffect(percentage);
+ }
/**
- * The object movement ends (confirm the stop position and which Index position should be sucked)
+ * 拖曳結束,確認停止位置並吸附至目標 Index
*/
private _dragEnd = (): void => {
if(this._configurator.setting.isDebug && logEnable.dragger.onDragEnd) logger.printInText('[Dragger._dragEnd]');
diff --git a/src/manager/Elementor/ElState.ts b/src/manager/Elementor/ElState.ts
index 749c04c..62d874e 100644
--- a/src/manager/Elementor/ElState.ts
+++ b/src/manager/Elementor/ElState.ts
@@ -154,23 +154,6 @@ class ElState {
};
- /**
- * 設定是否移動中
- * 加上狀態讓其他元素不會影響滑動
- * @param isEnable
- */
- setTouching = (isEnable: boolean) => {
- if(isEnable){
- if(this._elementor.rootEl){
- this._elementor.rootEl.dataset.touching = '';
- }
- }else{
- if(this._elementor.rootEl) {
- this._elementor.rootEl.removeAttribute('data-touching');
- }
- }
- };
-
diff --git a/src/styles.module.scss b/src/styles.module.scss
index 86ff043..916a368 100644
--- a/src/styles.module.scss
+++ b/src/styles.module.scss
@@ -11,16 +11,6 @@
- &[data-touching]{
- .pagination-group,
- .slide-item {
- pointer-events: none;
- }
- .nav-group{
- pointer-events: none;
- }
- }
-
&[data-gpu-render] {
.container{
will-change: transform;