---
# 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。
如果你需要：
1. 自動統計box的數量和記錄box的長度
2. 無視總長度
3. 設計好dim的邏輯
4. desktop Web 用translation 實現，mobile web 用scroll 實現
5. 點選好某個box，load 完以後自動歸到box的位置。
6. 不被熒幕大小而限制，會自動統計熒幕大小的改變而做出反應


## 基本設計與概念：
1. 如果熒幕大小為
2. 首先我們需要準備3個hook 和 2個button，與一整組的category box（我預設20個）

```jsx
const [currentRoll, setCurrentRoll] = useState(0);
//用來記錄global 目前roll的位置。refresh 則 reset，但考慮到rerender的時候應該是 onClick reload 或者 去其他的網頁，所以歸零沒問題。

const containerRef = useRef<HTMLDivElement>(null);
const sliderRef = useRef<HTMLDivElement>(null);
//用來探索目前div 的現況，其實就像 js的 document.querySelect 指向id 一樣。

```
3.我們來 hardcode 一組 array 作為 selector，方便接下來的作業。
```json
const queryTopRankApi = [
  {
    "id": "1",
    "name": "Google",
    "link": "https://www.google.com"
  },
  {
    "id": "2",
    "name": "YouTube",
    "link": "https://www.youtube.com"
  },
  {
    "id": "3",
    "name": "Facebook",
    "link": "https://www.facebook.com"
  },
  {
    "id": "4",
    "name": "Baidu",
    "link": "https://www.baidu.com"
  },
  {
    "id": "5",
    "name": "Wikipedia",
    "link": "https://www.wikipedia.org"
  },
  {
    "id": "6",
    "name": "Tencent QQ",
    "link": "https://im.qq.com"
  },
  {
    "id": "7",
    "name": "Tmall",
    "link": "https://www.tmall.com"
  },
  {
    "id": "8",
    "name": "Taobao",
    "link": "https://www.taobao.com"
  },
  {
    "id": "9",
    "name": "Amazon",
    "link": "https://www.amazon.com"
  },
  {
    "id": "10",
    "name": "Twitter",
    "link": "https://www.twitter.com"
  },
  {
    "id": "11",
    "name": "JD.com",
    "link": "https://www.jd.com"
  },
  {
    "id": "12",
    "name": "Instagram",
    "link": "https://www.instagram.com"
  },
  {
    "id": "13",
    "name": "Sina Weibo",
    "link": "https://www.weibo.com"
  },
  {
    "id": "14",
    "name": "LinkedIn",
    "link": "https://www.linkedin.com"
  },
  {
    "id": "15",
    "name": "Reddit",
    "link": "https://www.reddit.com"
  },
  {
    "id": "16",
    "name": "Netflix",
    "link": "https://www.netflix.com"
  },
  {
    "id": "17",
    "name": "Microsoft",
    "link": "https://www.microsoft.com"
  },
  {
    "id": "18",
    "name": "WordPress",
    "link": "https://wordpress.com"
  },
  {
    "id": "19",
    "name": "Pinterest",
    "link": "https://www.pinterest.com"
  },
  {
    "id": "20",
    "name": "Snapchat",
    "link": "https://www.snapchat.com"
  }
];
```

4. 建立html 和 css 的部分：
 ```jsx
 return (
     <div 
         ref={containerRef}
         className="w-full overflow-x-auto h-auto "
     >
     // containerRef 最大的寬度是熒幕的大小
         <div
             ref={sliderRef}
             className=" flex items-center justify-start px-2 gap-3"
         >
         // sliderRef 最大的寬度是所有item的總寬度，因為有masking，所以才 scrollable
             {queryTopRankApi.map(item) =>{
                 <button
                     id={item.id}
                     className="w-[200px] h-[50px] text-gold bg-white border border-solid border-gold hover:bg-gold hover:text-white"
                     onClick={()=>{
                         window.location.href(`${item.link}`)
                     }}
                     value={item.name}
                 />
             
             }}
        </div>
    </div>
 )
 ```
 ---
 ## 添加按鈕
按鈕會跟圖片重疊，所以會利用relative 和 absolute 來完成。這組可以添加在剛剛containerRef的上面。然後點擊以後會有一個handle去完成sliderRef的動態。
```jsx
<div className="relative w-full h-auto">
    <button 
        className="absolute top-1/2 -translate-y-1/2 left-4"    
    >
        <Image
            src={leftButton}
            size="100vw"
            className="w-[50px] h-auto aspect-square"
            width={0}
            height-{0}            
        />
    </button>
    <button className="absolute top-1/2 -translate-y-1/2 right-4">
        <Image
            src={rightButton}
            size="100vw"
            className="w-[50px] h-auto aspect-square"
            width={0}
            height-{0}            
        />
    </button>
</div>
```
當然如果想要hover的效果可以在按鈕的div上添加 onMouseEnter 和 onMouseExit，或者用 添加CSS hover:block hidden 去做交替哦。

---

## 設計slider的基本概念 ：

![image](https://hackmd.io/_uploads/B1u1AAxbA.png)

1. 我們需要一個 width：100% 的遮罩層，然後這是無法移動的層面。也是顯示層面。
2. 然後下面那層全裝按鈕，必須超過遮罩層。
3. 因為遮罩層的底層超過長度，那就達到scroll的條件。
4. 因為desktop格式希望達到是按按鈕左右滑動，所以desktop版本的，我們用 translateX 控 sliderRef.current
    - 所以我們在translateX的部分要做限制的條件。如果最左邊和最右邊，整個層面就不動。
    - 但計算右邊不滿整個畫面的長度時，就滑倒最後一個item的位置就好。（不造成破圖）
5. 如果要用scroll的話，那就簡單了，就不需要做左右滑動。直接做scrollbar拉動即可。

### 高階作業：
1. 當我點選某個按鈕的時候，可能會執行存檔/rerender/切換到其他的網站/也可能會增加queryString因此而刷新網站，這樣所有的hook都會被reset。
2. 設計需求可能是保留在當下選擇的按鈕位置。這樣就可能依賴：
    - middleWare做記錄
    - fetch到後端
    - 用localStorage/sessionStorage 來記錄當下的選擇（最好是SessionStorage，因為關掉瀏覽器就消失，畢竟這樣的記錄不重要）

---

## 前置作業：
1. 類似containerRef和sliderRef會經常使用，畢竟動態都跟它們有關係，所以建議統一做好，然後再呼叫。
2. 因為React 會做 onmount 第一次render，再做 updatemount 第二次render。如有其他相關值改變，會重複做render。為了避免所有的值重複render，所以拉開來做就不會出現額外的問題。

```jsx
    const calculateRefPosition = () =>{
        if(sliderRef.current && containerRef.current) {
            const containerWidth = containerRef.current.offsetWidth;
            const lastItem = sliderRef.current.lastElementChild as HTMLDivElement;
            const lastItemPosition = (lastItem.offsetWidth + 12) * sliderRef.current.childElementCount;
            const numRolls = Math.floor(lastItemPosition/containerWidth);
            
            return {containerWidth, lastItemPosition, numRolls };
        }
        return {containerWidth : 0,, lastItemPosition: 0 , numRolls: 0};
    }
```

- containerWidth 用來統計熒幕大小，這裡不使用width：100% 是因為可能存在 padding之類的，所以只取遮罩層的大小。
- lastItem 用來取sliderRef 那層最後一個 按鈕的所有屬性。
- lastItemPosition 用來記錄所有按鈕的綜合長度。不用sliderRef.current.offsetWidth 是因為這樣不含margin 或者 padding。單純取每個按鈕的長度 +12 按鈕之間的空檔空間。
- numRolls 是統計總數去掉小數點，看可以滑動多少次。以所有按鈕的長度 / 熒幕的寬度。例如熒幕 1300寬度，總長度3600的話，就有三面。

---
## 計算移動的位置：

```jsx
const calculateNewScroll = (
    direction: string,
    containerWidth:number,
    lastItemPosition:number,
    numRolls:number,
    newRolls:number) =>{
    
        let scrollPosition = 0;
        
        if(direction === "left") {
            scrollPosition = containerWidth * newRolls;
        }else if(direction === "right"){
            if((lastItemPosition > (containerWidth * newRolls)) &&
                (numRolls > newRolls)){
                scrollPosition = containerWidth * newRolls;
                }else {
                scrollPosition = (lastItemPosition - containerWidth) + paddingBox;
                }
        }
        return scrollPosition;
}
```

1. 這裡來解釋所有的props：
    - direction ： 左右點擊來決定接下來的運算。
    - containerWidth ： 得到熒幕的寬度
    - lastItemPosition ： 得到最後一個按鈕的位置
    - numRolls ： 總區隔
    - newRolls ： 目前的坐在區間

2. 建立一個暫時值來記錄。
3. 如果是左邊的話，到了最左邊，就不會有反應或回傳0.我這裡的寫法是 newRolls 將會在handling裡面處理得到目前的區間，然後再乘與區間。如果計算好的區間是0，那就會停留在第一個區間。
4. 左邊就比較單純，反倒右邊的話，就會有兩種判斷：
    - 如果最後的位置 比 現在的位置 大（簡單說就是沒到最後一格的時候） 並且 未到最大的區間那就現在顯示寬度 乘與 現在的roll。
    - 但如果下一個是最後的時候呢，就最後按鈕的位置 - 顯示寬度，這樣呢，我們就可以不會過度傳送超出的位置。
5. 因為判斷的關係，所以 scrollPosition 只有一個答案，最後呢，calculateNewScroll 將得到計算後的結果。

---

## 接下來是最後了 （handling）：

```jsx
const handleScroll = (direction: "left"|"right") =>{
    if(sliderRef.current && containerRef.current) {
        const {containerWidth, lastItemPosition, numRolls} = calculateRefPosition();
        let newRolls = currentRoll;
        
        if(direction === "left") {
            if(newRolls > 0){
                newRolls -= 1;
            }
        }else if(direction === "right"){
            if(numRolls > newRolls){
                newRolls += 1;
            }
        
        }
        
        const getNewScroll = calculateNewScroll( direction, containerWidth , lastItemPosition, numRols, newRolls);
        
        setCurrentRoll(newRolls);
        
        sliderRef.current.style.transform = `translateX(-${getNewScroll}px)`;
    }

}
```

1. 如果兩個div都讀不到，基本上這個功能就沒辦法執行了。其實一開始我沒有拆開來寫，現在兩個ref也不一定要放啦。![image](https://hackmd.io/_uploads/SyK3uPWbR.png)

2. 接下來就讀取那會重複用的值。因為用呼叫的方式，就**不會重複地算**。
3. 因為 **setState 無法馬上改值**，所以我們需要哪一個臨時的變數來承載state。但最後記得還是要把更新的結果給setState，在還沒render之前依然可以用。一來也可以**防止重複作業**。
4. 如果是左邊的並且符合條件的話，就馬上減一。如果是右邊的話，符合新的rolls 並沒有總rolls大，那就 +1，別忘了，這些***結果都是有變數直接統計***的。
5. 然後把結果都**傳入calculateNewScroll裡去做計算**後，把結果給 getNewScroll。
6. 別忘記給setState記載新數據。
7. 最後呢，請sliderRef 這個div去做移動的動作。

```jsx
<button 
        className="absolute top-1/2 -translate-y-1/2 left-4"  
        onClick={handleScroll("left")}
    >
        <Image
            src={leftButton}
            size="100vw"
            className="w-[50px] h-auto aspect-square"
            width={0}
            height-{0}            
        />
    </button>
    <button 
        className="absolute top-1/2 -translate-y-1/2 right-4"
        onClick={handleScroll("right")}
        >
        <Image
            src={rightButton}
            size="100vw"
            className="w-[50px] h-auto aspect-square"
            width={0}
            height-{0}            
        />
    </button>
```

1. 回到左右按鈕的地方植入 handleScroll並且標記左右就完成了！

那基本的desktop 依靠translate 移動的就完成了。如果是mobile用scroll的話，就不需要做這麼麻煩了，記得在 containerRef的那個div 下 overflow-x-auto 有超出顯示遮罩層就能scroll了哦。

這裡完成最基本的slider，如果想看進階的點擊造成rerender以後會回到點選的位置的話，可以看下一篇進階版本。

---

