generated at
表エディタをフルスクラッチした話

こんばんは
daiiz daiiz です


活動
生成AIと検索の自由研究
技術系同人誌の執筆


過去のKyoto.jsでの発表


株式会社HelpfeelでJavaScriptを書いています
Gyazo, Scrapbox, Helpfeel を作っている会社です


FAQ検索システム「Helpfeel」
Scrapboxに索引文を書いて変換すると賢く検索できるヘルプページになる
? (テーブルエディタ|表作成支援ツール|Table Maker)の開発話
このページをこんな感じで検索できる


モチベーション
ちょっとリッチな表を書きたい
できるだけ直感的に操作したい
見た目の調整にこだわりすぎないツールにはしたい


既存のツールやライブラリを調査
Editor.js
いい線いっている
あともう一息直感的にできそう
Quill
シンプルで格好いいが、tableは正式には対応していなかった
セル内のコンテンツの編集用エディタとして採用
Spreadsheet
当初はsvg-spreadsheetみたいなことも考えていた
ツールバーを操作をする以外のいいUIは作れないだろうか?
表現力が豊かすぎる!


つくるぞ!
Table Maker
完全にイチから設計する挑戦のまとめ

デモ
実際に動かしながら紹介する
左に資料、右にデモ

デモ素材
SVGファイルが生成される
これを読み込んで編集を再開できる
この程度の表現力がほしい
セル結合
セル内に画像
セルに背景色
セル内の文字のセンタリング


生成物
こんな感じのSVG画像
svg
<svg> <foreignObject> <html> <style>...</style> <table>...</table> </html> </foreignObject> </svg>
foreignObjectタグには任意のHTMLを書けることを利用する
外部画像を埋め込む場合はbase64エンコードしておくとポータビリティが高まる
imgタグで普通に表示できるのでScrapboxでもプレビューできる
例: 冒頭のファイル
SVG Screenshotでの知見が活きた


ここからはデモとともに解説
こだわりとからくり
いかに複雑化せずに作っていくか


table要素で表現
ツールと生成物ともにHTMLの <table> タグを使う
CSS Grid Layoutでもおそらく実現できたけれどやめた
古から使われているtableの方があらゆる環境での表示の差異が小さそう
プレーンなHTMLで完結していると、WordやSpreadsheetなどの他のツールにも取り込みやすい

思わぬ嬉しさ
入出力をHTMLのtableにしたことで、外部のウェブサイトからコピペで取り込む機能が作りやすかった



ポイント
各セルに操作用のハンドルとしてdiv要素が付いている
セルの右と下の2辺だけに配置すればよい
これらを組み合わせて、以降で解説する操作を実現できる


セル 結合
一番の難所
モードの切替えなく実現できたのがよかった
隣接するセルの仕切りを取り払うことで結合していく


セル結合情報の持ち方
セルの状態を表す2次元配列の表現
結論
<td colspan="列の結合数" rowspan="行の結合数"></td> に倣うのが最も扱いやすい
やはりHTMLはよく考えられている
結合範囲の左上のセルで両方向の結合量を持つ
他のセルでも便宜上の値を持っておく
0 or 1
1: 自身が上または左の辺を構成している場合
0: それ以外
例: 括弧内の値は (colspan, rowspan) を表す
列のみ結合
行のみ結合
両方向で結合


セル 結合の解除(分割)
一番の難所
結合時に取り払った仕切りを再建する感覚で操作する


セル 選択と移動
クリック
矢印キー
結合されたセルが登場すると難しくなる


一貫性と可逆性
矢印キーでの移動
結合されているセルを跨いだときの挙動に気を遣う
同じ列や行内で一貫した選択操作ができているかが大事

例1: 「Scrapbox」のセルがある3列目での下移動
横方向に結合されたセル(5行目)を通過した直後に、3列目の「Node.js」が選択される
通過後に2列目 (横方向の結合された一番左のセルの位置) に移動しないよう注意
例2: 「Helpfeel」のセルから下移動を開始して跳ね返った後
元の位置に戻ってこれるか?



Undo / Redo
Ctrl + Z, Ctrl + Shift + Z で何度でもやり直せる
スナップショット方式
一番簡単な実装方法
各操作後のテーブル全体の結果をすべて蓄積しておき、逆順に適用するだけで実現可能
Undo / Redoに関する一般的でさらに詳しい話はこの資料がおすすめ


セルの結合の分割は「実質Undo」
セルの結合・分割の連続操作はよく観察するとUndo的な挙動をしている
分割は結合の逆操作である

普通に分割すると
結合されていたセル内のテキストや画像は分割後のどちらのセルに属するべきか?
一番シンプルな解決法: 規則的にどちらかに寄せる
分割操作としては間違いではないが、結果に違和感がある

より直感的に分割するには?
Undoの一種として扱って分割する
結合前に所属していたセルへの再割り当てが可能
直前までの内容が復元されて気の利いた賢い動きになる


「実質Undo」の実現方法
分割前後のテキスト履歴を各セルで個別に保持することなく、Undoを応用して実現できる
操作履歴 分割前
状態 Data ------------------- s0 data0 s1 (結合) data1 <-- 最新
状態履歴から復元先(s0)を適用する
この際に分割 (結合の逆操作) の履歴s0'を作って挿入しておくと、さらにUndo/Redoされても辻褄が合う
操作履歴 分割後
状態 Data ------------------- s0' (分割) data0 s1 (結合) data1 s0 data0 <-- 最新

まとめ
操作や挙動を細かく観察する
セルの結合と分割をUndo / Redo操作と対応付ける
シンプルなツールにするためにいろいろ考える
より一般的なデータ表現
ポータビリティ
最高に直感的に使える表エディタができた


おまけ: 比較的簡単な操作集

行や列 追加と削除
外側に不可視状態で行と列が1個ずつある
番兵的に機能している
表の基本構造を維持している
目的のセルの仕切りの位置にカーソルを持っていくだけでよい
その位置でできる操作だけがサジェストされる
必要なときだけ見せる
ボタンを作用点の近くに出す
「いま何ができるのか」が明確になる


セル内の編集
Quill (ReactQuill) を使っている
セルごとに文字や画像を編集できる
画像の大きさを調整する機能は自前で実装
ドラッグしてリサイズする処理はreact-rndを使うと最高に書きやすい



横幅をいい感じにする
作成した表全体の横幅を統一したいときに手軽に調整できる
各列の横幅が、それぞれの割合に応じていい感じに伸び縮みする