---
# System prepended metadata

title: CustomCategories 插件作業（進階版）

---

CustomCategories 插件作業（進階版）
===
<div style="position: relative; padding-bottom: 56.25%; height: 0;"><iframe src="https://jumpshare.com/embed/HhncWqIJ9Jk4llL9PFSu" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe></div>

---
## 這不是插件：
這是我自己研究和 base on React的格式去撰寫的一組slider。

上一章是說基本款。接下來這裡將介紹進階版：
《**點選以後發生Rerender將回到點選的位置**》
如果上一章還沒看呢，建議點擊[這裡](https://hackmd.io/@myrealstory/SyAbmHae0)看看上一章。

## 概念講解：
- 要先記錄每一個按鈕的位置，供點擊時使用。
- 當點擊了按鈕以後，取得ID然後馬上取得當下按鈕資訊，，並且馬上記錄到存放的地方。
- 當回到這個頁面的時候，嘗試讀取是否有記錄，如果有就馬上轉移到該按鈕的位置，這個方式適用 desktop 或 mobile 版本。

## 先做讀取每個按鈕的位置：

```jsx
const [buttonPosition, setButtonPosition] = useState<Record<number, {id:number, position:number}>| null>(null);

useEffect(()=>{
    if(sliderRef.current) {
        const position： Record<number,{id:number,position:number}>= {};
        data.forEach((value,index) =>{
        const button = sliderRef.curent!.children[index] as HTMLDivElement;
        position[value.id] = {id:value.id, position: (button.offsetWidth + 8 ) * index};
        });
        setButtonPosition(position);
    }

},[data]);
```

1. Record<TKey,TType> 是 TS一個泛型工具類型，用來創建一個物件類型，其中物件的key是"TKey" 類型，且每個鍵對應的值(value)是 TType類型。這裡的Record用來確定 buttonPosition 可以是一個 object，然後object的每個key 都是 number類型，且每個key 都會映射到TType裡。
2. 然後建立一個臨時變數來記錄運算過程。整合以後再丟到setState裡面以便記錄。
3. 這裡選擇forEach的原因是因為需要針對data陣列中的每一項元素執行操作，不需要製造新的array，所以靠foreach讀取就好。
4. 
```jsx
const button = sliderRef.current!.children[index] as HTMLDivElement;
positions[value.id] = { id: value.id, position: (button.offsetWidth + 8) * index };

```
- button 變數記錄從 sliderRef.current 的子元素中取得對應的按鈕元素，根據 index。
- 計算每個按鈕的位置，位置是基於按鈕的寬度加上一定的間隔乘與以它的index.
- 將每個按鈕的 id 和計算以後的 position 存儲到 positions的object裡。

5. 然後更新 setButtonPosition。
6. 每當data有更新，就會執行一次。（客戶很愛對比現在和CMS更新以後是否能即時反應）


## 點擊以後，尋找當下的位置記錄：

```jsx
const handleButtonClick = (id:number) =>{
    if(buttonPosition === null) return;
    const positions = buttonPosition[id] ? buttonPosition[id].position : null;
    const {containerWidth} = calculateRefPosition();
    const boxSize = sliderRef?.current?.lastElementChild?.clientWidth;
    const currentRoll = position && boxSize ? Math.floor((position + boxSize)/containerWidth) : 0;
    
    const storageObject = {
        position : positions,,
        currentRoll: currentRoll,
        id:id,
        width:width,
    }
    
    if(position !== null) sessionStorage.setItem("categoryPosition", JSON.stringify(storageObject));
    
}
```

1. 如果 buttonPosition 沒有東西，那這個功能就廢掉了。
2. 然後建立一個變數用來記錄當下傳入 handleButtonClick 的id ，但也要看buttonPositions是否有漏掉的，如果沒有就回傳 null。
3. 接下來就取得 顯示大小。
4. 然後計算當下的所屬區間，如果位置不超過顯示位置，自然在0號區間，如果超出了，就滑到當下的區間因此而記錄。
5. 如如果資料準備好，然後上傳到 sessionStorage 裡。
6. (update 4/23) 追加 boxSize，因為這裡統計的position 並不包含所有盒子的寬度，只是盒子的最左邊的。所以還要加上盒子的長度，才會成為完成的sliderRef 長度（這裡指的是最後一個盒子。）

---
## 重點！

```jsx
const width = window?.visualViewport?.width ?? 0;

useEffect(()=>{
    
    const currentCategory = JSON.parse(sessionStorage.getItem("categoryPosition") || "{}");
    const storedItemId = currentCategory.id || 0;
    const storeWidth = currentCategory.width || 0;
    
    if(storedWidth !== width) {
        handleButtonClick(storedItemId);
    }
    
    const storedPOosition = currentCategory.positions || 0;
    const storedRoll = currentCategory.currentRoll || 0;
    const {containerWidth, lastItemPosition,, numRolls} = calculateRefPosition();
    const containerSlider = containerRef.current;
    let currentPosition = 0;
    
    if(storedPosition !== 0 && containerSlider && sliderRef.current && width > 0) {
    if(width < 768) {
        setTimeout(()=>{
        containerSlider.scrollTo({
            left: storedPosition,
            behavior: "smooth",
            })
        },100);
    }else{
        if(parseInt(storePosition) > containerWidth && numRolls > storedRoll){
            currentPosition = containerWidth * (Math.floor(parseInt(storedPosition)/ containerWidth));
        }
        if(numRolls === storeRoll) {
            currentPosition = lastItemPosition - contianerWidth;
        }
        
        sliderRef.current.style.transform = `translateX(-${currentPosition}px)`;
        setCurrentRoll(storedRoll);
    }
    }

},[buttonPosition, width]);
```

1. 這裡是需要re-render過程時要及時觸發的。
2. width的部分呢，會在另一個章節介紹我自己寫的 customHook ，用來統計目前熒幕高寬和目前scrollY的高度 和 某些客制化用來隱藏bar的機能。
3. 但目前先用直接取 window width 做法。
4. 先做好在sessionStorage裡面值的分類。
5. 這裡先做個小小的預防，防客戶很愛拉來拉去去做比較，所以寫了個保險：如果熒幕寬度被改變的話，那就重新取值，再跑一次。
6. 如果 storedPosition 不是0，在讀取到 div 和 顯示寬度大於 0，這裡要注意：**因為window 的 visualViewport 一開始都讀不到，所以必定是0，等讀到了寬度，再執行**。
7. 當熒幕是mobile web的時候，因為改用scroll，所以我們要控overflow-x-auto 的那個div，然後進行scrollto 的動作。這裡需要注意的是，要等所有的值都準備好了，才scroll，故設計 setTimeout延後執行。
8. 到desktop web 的部分，我們只要回到那個區間就好。所以 只要點擊的位置比熒幕還要大的話，就把哪個區間的位置傳到 currentPosition 裡。
9. 如果 最大的區間跟現在區間是一樣的話，那就最大的長度減去熒幕寬度。如圖：
![image](https://hackmd.io/_uploads/SJF2ZYfWA.png)
那就可以如設想一樣移動到最後一格。
10. 最後記得要更新現在的roll，以便接下來有其他的動作。不然就會出現意想不到的結果。

---

## 總結：
我覺得這個slider並沒有很完善，也因為是這次作業被要求要有這樣的效果。但我覺得可以按照這樣的思考方向去找到出路，畢竟想要完成slider，先了解底層運作和設計才能做出更好的 hook 和效果！ 大家加油和感謝把文章看完！希望大家都有收穫。
![image](https://hackmd.io/_uploads/rkeM4YM-C.png)
