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的基本概念 :

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也不一定要放啦。
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以後會回到點選的位置的話,可以看下一篇進階版本。
---