表エディタをフルスクラッチした話
Kyoto.js #19での発表資料です
こんばんは
活動
生成AIと検索の自由研究
技術系同人誌の執筆
過去のKyoto.jsでの発表
株式会社HelpfeelでJavaScriptを書いています
Gyazo, Scrapbox, Helpfeel を作っている会社です
FAQ検索システム「Helpfeel」
Scrapboxに索引文を書いて変換すると賢く検索できるヘルプページになる
このページをこんな感じで検索できる
モチベーション
ちょっとリッチな表を書きたい
できるだけ直感的に操作したい
見た目の調整にこだわりすぎないツールにはしたい
既存のツールやライブラリを調査
Editor.js
いい線いっている
あともう一息直感的にできそう
Quill
シンプルで格好いいが、tableは正式には対応していなかった
セル内のコンテンツの編集用エディタとして採用
Spreadsheet
当初はsvg-spreadsheetみたいなことも考えていた
ツールバーを操作をする以外のいいUIは作れないだろうか?

表現力が豊かすぎる!
つくるぞ!
Table Maker
完全にイチから設計する挑戦のまとめ
デモ
実際に動かしながら紹介する
左に資料、右にデモ
デモ素材
SVGファイルが生成される
これを読み込んで編集を再開できる
Gyazo | Scrapbox | Helpfeel | ||
スクリーンショットの瞬間共有 | チームのための新しい共有ノート
| 検索性能に特化した新世代FAQ検索システム
| ||
静止画撮影 | 動画撮影 | |||
概要 | Gyazoはスクリーンショットを使ったコミュニケーションと情報収集のためのツールです。 ユーザー構成は欧米を筆頭に海外比率約86%、総キャプチャ数9億枚以上と世界中で利用されています。 | Scrapboxでは企画書、社内マニュアル、議事録など、チームに必要なドキュメントを共同で瞬時に作成できます。 ドキュメント同士がリンクを元につながり、何千、何万ものドキュメントを管理する苦労から解放してくれることが特徴です。 | Helpfeelは検索性に特化し、問題がすぐに解決する「FAQ」を簡単に構築できるシステムです。ユーザーが自力で問題を解決するのを手助けするだけでなく、CS担当者やコールセンターの負担も削減します。 | |
フロントエンド | React JavaScript / TypeScript
| |||
バックエンド
| Ruby on Rails Go(画像処理) | Node.js | Node.js Python(自然言語処理) | |
MongoDB | ||||
インフラ | Google Cloud Platform | Heroku Google Cloud Platform
|
この程度の表現力がほしい
セル結合
セル内に画像
セルに背景色
セル内の文字のセンタリング
生成物
こんな感じの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に関する一般的でさらに詳しい話はこの資料がおすすめ
/guiland/自動勉強会 vol.2 共同編集・Undo回
セルの結合の分割は「実質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を使うと最高に書きやすい
横幅をいい感じにする
作成した表全体の横幅を統一したいときに手軽に調整できる
各列の横幅が、それぞれの割合に応じていい感じに伸び縮みする