Day 11 拖曳紙牌的效果(三)如何一次拖曳多張卡牌

今日預計只實作如何一次拖曳 多張卡牌 多張卡牌拖曳的考察研究 這部分可能會讓很多人(我)失望,因為vue.draggable.next最近一次的合併更新在2021年8月, 所以目前Vue3無法像原本Vue2能使用vue.draggable的Multi-drag的擴充功能,所以我捨棄使用套件原生多筆拖曳的想法和 拖曳多張牌完美的畫面效果。 如果願意改變資料結構為巢狀Vue3版本還是有辦法對巢狀物件進行一次性的拖曳,但對我來說在未來資料處理的靈活性降低又提高判斷卡牌順序的複雜度因此不考慮。 想法邏輯 從來源牌堆先拖曳一張牌A移動到目標牌堆的指定位置 將來源牌堆中A牌後的剩餘卡片複製到目標牌堆的指定位置後方 刪除來源牌堆原A位置後的剩餘元素 實作後發現其實可以先複製一份來源牌堆、目標牌堆移動後的結果,後續處理會更為靈活。 實作邏輯 在:move對應的函數中判斷可拖曳時,產生『若拖曳成功後,來源/目標陣列的新狀態』並封裝成一個箭頭函數儲存至ref變數changeOption。 function limitLocalMove(evt) { // 限制同個牌堆無法拖曳 const result = evt.from !== evt.to; if (result) { // 取得牌堆的來源、目標名稱,對應reactive`cardStacks`內的名稱 const from = getDomName(evt.from); const to = getDomName(evt.to); const draggedContext = evt.draggedContext const { index, futureIndex } = draggedContext; // 產生多筆拖曳後,來源牌堆、目的牌堆的陣列變動後的結果 const newFromCards = cardStacks[from].slice(0, index); const newToCards = [ ...cardStacks[to].slice(0, futureIndex), ...cardStacks[from].slice(index), ...cardStacks[to].slice(futureIndex) ]; // 將變動牌堆的函數暫存,預計等到拖曳完成後執行 changeOption.value = () => { cardStacks[from] = newFromCards; cardStacks[to] = newToCards; changeOption.value = null; }; } // 仍使用原生的拖曳效果 return result; } 當@change事件發生時,若定義ref變數changeOption有值則執行第1點暫存的函數,將最終結果更新對應的牌堆陣列之中。 @change只有在卡牌拖曳完成對陣列產生異動時才會自動被執行,此為draggable原始機制 function cardChange(event) { if (changeOption.value) { changeOption.value(); changeOption.value = null; } else { console.log(`no trigger changeOption`); }; } 樣板調整除了<draggable>的屬性:move、@change之外,眼尖的朋友可以注意到兩個<draggable>分別添加了ref="first" 和ref="second"對應到第一/第二牌堆的元素,主要是為了第1點在:move函數執行中可以判斷來源和目標陣列對應到cardStacks內的哪一個陣列。 <GameBoard> <div> <h4 class="title">牌堆1</h4> <draggable :list="cardStacks.first" group="pokers" itemKey="value" style="display: grid; grid-template-columns: repeat(13, 3rem); background-color: yellow;" :move="limitLocalMove" @change="cardChange" ref="first"> <template #item="{ element, index }"> <Card :value="element.value" :isOpen="element.isOpen" /> </template> </draggable> </div> <div> <h4 class="title">牌堆2</h4> <draggable :list="cardStacks.second" group="pokers" itemKey="value" style="display: grid; grid-template-columns: repeat(13, 3rem); background-color: yellow;padding: 1px;" :move="limitLocalMove" @change="cardChange" ref="second"> <template #item="{ element, index }"> <Card :value="element.value" :isOpen="element.isOpen" /> </template> </draggable> </div> </GameBoard> 因為:move本身攜帶的資訊無法得到拖曳來源、目標的:list,但卻有紀錄拖曳列表來源、目標的DOM(from,to),所以撰寫一個函數可取得DOM所對應的牌組名稱。 ...

September 20, 2023 · 2 min · 宗嘉

Day 10 拖曳紙牌的效果(二)限制內部拖曳

今天來研究兩個列表的拖曳和如何限制列表的拖曳,學習內容主要來自vue.draggable文件的內容和親身實作進行分析。 先實現兩個列表的拖曳 昨天已學過如何製作單一列表的拖曳,先將相同的<draggable>內容複製往下貼, 複製的另一個<draggable>只需要調整屬性:list成想要對應的另一個陣列,並且兩個<draggable>添加相同的屬性group則自動實現兩個列表拖曳的關聯。 <template> <div> <draggable :list="firstCardStack" group="pokers" itemKey="value"> <template #item="{ element, index }"> <Card :value="element.value" :isOpen="element.isOpen" /> </template> </draggable> </div> <div> <draggable :list="secondCardStack" group="pokers" itemKey="value"> <template #item="{ element, index }"> <Card :value="element.value" :isOpen="element.isOpen" /> </template> </draggable> </div> </template> 如何限制列表的拖曳 前面已先實現了兩列表的拖曳,但為求理解仍保留著@change="console.log",在限制拖曳之前,我必須先觀察拖曳完成前能得到的資訊,所以我必須先觀察變動後@change實際印了什麼有用的資訊。 因為如果只是要單純限制一個<draggable>不能拖曳移入移出,只需要設定屬性:move="() => false",但我想做的不僅僅如此。 不同牌堆的移動 將牌堆1的梅花A移動到牌堆2後,可觀察到開發人員工具的Console依序印出兩個物件: 內含added屬性,且added物件本身包含陣列移動的元素element和目標陣列內的新位置newIndex 內含removed屬性,且removed物件本身包含陣列移動的元素element和原陣列內的舊位置oldIndex 這兩個@change是由各自受影響的Draggable所觸發,但內含的元素是相同的 同牌堆的移動 將同牌堆的第三張牌(梅花3)移動到第六張牌(梅花6)的位置,則只印出一個包含屬性moved的物件, 且moved物件本身包含陣列移動的元素本身element和新位置newIndex和舊位置oldIndex的資訊。 從此可推測,如果要禁止同牌堆內的移動,只需要禁止moved的行為。 禁止同牌堆的內部拖曳 從官方文件中可查到屬性:move對應的函數只要回傳false即可取消此次的拖曳。 function onMoveCallback(evt, originalEvent){ ... // return false; — for cancel } 這邊我只印出參數evt得到的資料,內容跟@change不一樣的超乎想像多資訊😫,而且跟@change不同的是,這個:move對應函數只要拖曳卡牌到某一張卡的上面就會觸發一次 ...

September 19, 2023 · 1 min · 宗嘉

Day 09 拖曳紙牌的效果(一)

玩紙牌接龍最重要的就是卡牌會移來移去,之前都是用點的移動定點, 今天來試試看如何撰寫拖曳紙牌的功能。 安裝依賴 因為重頭學習理解拖曳,對我來說太麻煩也太無聊, 乾脆就使用現成的套件Vue.Draggable吧! 因為使用的是Vue3專案,所以必須安裝有標註next的版本。 npm i -S vuedraggable@next 單一列表的拖曳使用 我也還在理解該套件中,接下來的過程會盡可能去蕪存菁,但不失細節。 首先一定要先引入vuedraggable,如下所示: <script setup> import draggable from 'vuedraggable' // ...other template </script> 樣板的部分則如下: <template> <draggable :list="firstCardStack" itemKey="value" @change="console.log" style="display: grid; grid-template-columns: repeat(13, 3rem);"> <template #item="{ element, index }"> <Card :value="element.value" :isOpen="element.isOpen" /> </template> </draggable> </template> 這樣的寫法在通常就已可運用,接下來讓我們來逐一理解<draggable>每個欄位的意義和預設行為… :list 首先:list內設定的是一個參考到陣列的ref或reactive變數,只要裡面參考到的是Array即可, 這可以讓<draggable>明白當列表項目被拖曳移動時自動修改的對象。 // 此處genearateDeck(20, true)會回傳20個物件的Array物件 const firstCardStack = ref(geneateDeck(20, true)); // 為求簡單也能寫成底下這樣 // const firstCardStack = ref([]); itemKey屬性 第二個重要的屬性是itemKey,代表前面陣列中每一個元素的唯一值,基本上就跟v-for內用到的:key有相同作用,可讓元件從itemKey明白內部元素的差異進而去做列表更新。 此處會設定itemKey="value",是因為參考的陣列firstCardStack每一個元素的構造如下: { value: number, // 撲克牌值,Ex: 0 對應 ♣A isOpen: boolean // 開牌狀態,Ex: false 對應 蓋牌樣式 } 嘗試過不添加itemKey仍可拖曳且參考的陣列有更新,但會發生UI不會刷新的窘境🤣 ...

September 18, 2023 · 1 min · 宗嘉

Day 08 牌堆的卡牌移動動畫

重點提醒: 沒有真的成功實作卡牌無中斷的移動 在實作的過程中遇到了一些問題,找到替代方案先記錄下來。 最初的思路 在前一篇文章中,我們已經完成了牌堆的製作,接下來我們要來製作牌堆的卡牌移動動畫。 在開始實作之前,我必須決定要用CSS或是JS實現? 如果牌堆的位置永遠是固定的,或許可以用CSS來實現,但是我們要移動的是牌堆位置不一定永遠在那, 而且牌堆的卡牌數量是不固定的,更加深了預設的座標位置,所以我想這必須用JS來實現, 至少需要JS取得動態元素的位置。 動畫的實作 第一個問題對我來說是,我要怎麼知道卡牌的位置? 起始和結束的位置都是不固定的,所以我必須要先取得卡牌的位置,才能夠進行動畫的實作。 取得當前牌堆的位置的方法有很多種,我在這邊使用的是getBoundingClientRect(), 這個方法可以取得當前元素的相對位置+寬高,但是這個方法是在DOM上的,所以我利用const cardBox = ref(null)設定一個參考, Vue3會自動找到template中使用ref="cardBox"的元素,並且將其綁定到cardBox上,這樣我就可以在<script setup>中使用cardBox.value來取得HTML元素本身。 因為元件渲染掛載畫面上便會觸發onMounted事件,所以我在onMounted中取得元素的位置,並且透過emit傳遞卡片的絕對位置給父元件。 至於為什麼是絕對位置,因為我們要移動的是卡片,而不是牌堆,所以我們必須要知道卡片的絕對位置,才能夠進行移動。 雖然後來我失敗了,但是我還是想記錄一下這個方法,因為我覺得這個方法還是很有用的。 // CardColumn.vue <script setup> // ... const cardBox = ref(null); onMounted(() => { const rect = cardBox.value.getBoundingClientRect(); const x = window.scrollX + rect.left; const y = window.scrollY + rect.top; emit('position', { x, y }); }) </script> <template > <div class="card-box" :class="{ 'empty-card-box': isEmpty }"> <div class="card" style="visibility: hidden;"></div> <div style="visibility: visible; position: absolute; z-index: 5;" ref="cardBox"> <div style="; display: grid; grid-template-rows: repeat(13, 2rem);"> <Card v-for="(card, index) in cards" @click="(event) => onClick(event.target)" :key="card.value" :value="card.value" :isOpen="card.isOpen" /> </div> </div> </div> </template> 然後在上層HomeView.vue就寫@position接收到x,y位置變數後,基礎工作就完成了。 ...

September 16, 2023 · 2 min · 宗嘉

Day 07 卡牌垂直重疊

今日完成目標 多張卡牌實現垂直重疊,但露出非交疊的部分 垂直重疊 為了產生重疊的效果,個人覺得最酷的方式應該是使用CSS的grid排版, 所以在這之前我利用一個GRID GARDEN的網站練習了一下。 原本問ChatGPT是得知用每一張卡堆疊都還要套用不同的CSS,如果要多一張牌就要多一個class,或是用sass的寫法達成,但以上這些我都不想要,一來太麻煩、二來sass還要額外引入新依賴,畢竟我只是想堆疊卡牌而已。 接著說明實際我達成的方式是靠display: grid;要求格狀排列,然後設定grid-template-rows為 repeat(13, 3rem);,這樣就可以讓每一張牌所在的格子都只有3rem的高度,設定13是因為我認為這樣垂直重疊排列卡牌最多只有13張,畢竟紙牌接龍不同花色暫時串再一起也就13張,在現實還是虛擬我的認知都是這樣,當然在設定比13高一點也不會影響,但如果出現第14、15張就會有卡牌不重疊,這點還請注意。 那為什麼達成重疊呢?我在容器內裝13個元件,每一個元件都只裝一張牌,且元件高度限制都在3rem,但實際元件 牌的高度是超過3rem,所以當我裝入第二張牌就會擋住第一張牌,讓第一張牌只露出3rem的高度,以此類推,最後一張牌則會露出全部。 // CardColumn.vue <template > <div class="card-box" :class="{ 'empty-card-box': isEmpty }"> <div class="card" style="visibility: hidden;"></div> <div style="visibility: visible; position: absolute; z-index: 1;"> <div v-if="isEmpty">沒牌</div> <div v-else style="; display: grid; grid-template-rows: repeat(13, 3rem);"> <Card v-for="(card, index) in cards" @click="onClick" :key="card.value" :value="card.value" :isOpen="card.isOpen" /> </div> </div> </div> </template> 額外補充可以注意到card-box內第一個元素是用來稱基本的寬高, 所以第二個元素我則讓他設定position: absolute這樣可以讓其中的格狀排列不會受到第一個元素的影響也不會影響到外部元素。 若少掉position: absolute的話,會變成底下這樣: ...

September 15, 2023 · 1 min · 宗嘉