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