iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0
Software Development

從零打造網頁簡報編輯器系列 第 6

Day 06|新增切換與排序多張投影片

  • 分享至 

  • xImage
  •  

系列:一張投影片背後的工程:從零打造網頁簡報編輯器

對應程式版本:Git tag day-06

版本說明:本文記錄當日實作;後續加入略過投影片等功能時,仍沿用相同的 ID 與順序模型。

一張投影片已經能顯示文字與圖片,接下來要把它變成真正的簡報:使用者必須能新增、切換、複製、排序與刪除頁面。這些操作看似只是改變左側縮圖,實際上必須同時維持顯示順序、投影片內容與目前頁面的關係。

Edge 顯示三張投影片、目前頁面與投影片操作按鈕

左側縮圖依文件順序顯示,藍色背景標示目前頁面;下方按鈕負責複製、前移、後移與刪除。

頁碼不是投影片身分

文件使用 slideOrder 保存順序,slides 則依固定 ID 保存內容:

interface PresentationDocument {
  slideOrder: string[]
  slides: Record<string, Slide>
}

例如 ['slide-a', 'slide-b'] 改成 ['slide-b', 'slide-a'],只是兩個 ID 交換位置,投影片內容不用搬動。畫面上的頁碼由陣列索引加一算出;排序後頁碼會改變,ID 不會。

左側清單也用這個 ID 當 React key

{presentation.slideOrder.map((slideId, index) => (
  <button
    key={slideId}
    aria-label={`投影片 ${index + 1}`}
    aria-current={slideId === activeSlideId ? 'page' : undefined}
  >
    <SlideRenderer slide={presentation.slides[slideId]} />
  </button>
))}

React 因此能在插入、移動或刪除後辨識同一張投影片。activeSlideId 則是介面狀態,只表示現在打開哪一頁,不需要寫進簡報文件。

五種操作各自改變什麼

新增投影片時,用 crypto.randomUUID() 產生新 ID,把空白頁加到順序尾端,再切換到新頁。切換縮圖只改變 activeSlideId,文件本身完全不變;同時清除物件選取與文字編輯狀態,避免上一頁的 UI 狀態殘留到下一頁。

複製需要比新增多一步。新投影片與其中每個物件都取得新 ID,之後編輯副本才不會改到原頁。文字樣式也建立新物件;圖片物件則保留原本的 assetId,讓兩頁共用同一份 Blob,不必複製大型圖片。

const copy = {
  ...activeSlide,
  id: makeId('slide'),
  elements: activeSlide.elements.map(duplicateElement),
}

排序只更新 slideOrder。Reducer 先複製陣列,再用 splice 移除原位置並插入目標位置;slides 物件維持原參照。目標索引會限制在第一頁到最後一頁之間,邊界上的前移或後移按鈕也直接停用。

刪除後要有可用的目前頁面

刪除目前頁面前,介面先找下一頁;若目前已是最後一頁,就改用上一頁。Reducer 再同步移除 slideOrder 中的 ID 與 slides 裡的內容,避免留下無法到達的紀錄。

最後一張投影片不能刪除。這項規則同時存在於按鈕狀態和 reducer:按鈕提供立即回饋,reducer 則保護所有呼叫路徑。即使未來加入快捷鍵或其他入口,文件仍至少保留一頁。

用狀態與介面測試固定規則

Reducer 測試確認指定位置插入、順序移動、刪除內容紀錄,以及拒絕刪除最後一頁;介面測試則實際新增並切換頁面,確認兩頁不會共用物件,也驗證複製內容、刪除後的目前頁面及邊界按鈕。

npm run validate:edge 另外在 Edge 執行複製、排序與刪除流程,縮圖內容和自動儲存皆通過。day-06 當時共有 6 個測試檔、24 個案例通過,lint 與 production build 也都成功。

下一篇會回到投影片內部,整理文字與圖片從建立、選取、拖曳到保存時共用哪些操作,又有哪些資料必須分開處理。

參考資料


上一篇
Day 05|選擇畫布的呈現方式
下一篇
Day 07|把文字與圖片變成可操作的物件
系列文
從零打造網頁簡報編輯器13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言