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)