TssGrid MIT

Excel の「右下を掴んで引っぱる」を軽量グリッドで実装する

設計 株式会社スリースターソフトウェア

Excel で 12 を選んで、選択枠の右下の小さな四角を下へ引っぱる3, 4, 5… と連番が流れていく――あの操作を「フィルハンドル」と呼びます。 毎日 Excel を触っている人にとっては息をするように使う機能なので、 業務向けの Web グリッドでこれが無いと、それだけで「使えない」と判断されます。 この記事は、TssGrid がこのフィルハンドルを依存ゼロ・コア数十行でどう組んだか、 そして地味に効いてくる設計の勘所の記録です。

結論から: フィルは ①ポインタの幾何から「どの矩形を埋めるか」を決める関数と、 ②元の値から「何を流すか(連番/コピー/タイル)」を決める関数2つに割ると素直になります。 そして全部の書き換えを1つの履歴コマンド(=1回の Undo で戻る)に通すこと。この3点です。

フィルが「やっていること」を分解する

ドラッグ操作はリッチに見えますが、確定の瞬間にやることは2つしかありません。

  • どこを埋めるか=元の選択範囲(src)と、いまポインタがある位置から、塗りつぶす矩形(target)を決める。
  • 何で埋めるか=src のセル値を見て、連番にするか・そのままコピーするか・パターンを繰り返すかを決め、target に流し込む。

この2つを別々の関数にしておくと、ドラッグ中のプレビュー枠(どこが塗られるかを薄く見せる箱)は 前者だけを呼べばよく、確定時は前者+後者を呼ぶ、という形に綺麗に分かれます。

解法①:方向は「はみ出し量が大きい方」へスナップする

いちばん最初に悩むのが方向です。右下のハンドルを斜め下に引っぱったとき、 ユーザーは「下に連番を入れたい」のか「右に入れたい」のか。Excel は斜めには塗らず、 ドラッグ方向の支配的な軸へスナップします。TssGrid も同じで、 縦のはみ出し量と横のはみ出し量を比べ、大きい方の軸だけを採用します。

// src=元の選択範囲, (pr,pc)=ポインタのいるセル。塗る矩形を返す。
fillRectFromPointer(pr, pc, src) {
  if (insideSrc(pr, pc)) return null;          // 範囲の中=何もしない
  const dDown = pr - src.r1, dUp = src.r0 - pr;   // 下/上へのはみ出し
  const dRight = pc - src.c1, dLeft = src.c0 - pc; // 右/左へのはみ出し
  const vOver = Math.max(dDown, dUp, 0);
  const hOver = Math.max(dRight, dLeft, 0);
  if (vOver >= hOver)   // ★ 縦のはみ出しが勝てば縦フィル、横が勝てば横フィル
    return (dUp > dDown) ? rect(pr, src.r1, src.c0, src.c1)
                       : rect(src.r0, pr, src.c0, src.c1);
  return (dLeft > dRight) ? rect(src.r0, src.r1, pc, src.c1)
                         : rect(src.r0, src.r1, src.c0, pc);
}

この「はみ出し量で軸を選ぶ」だけで、斜めドラッグでも自然に縦/横へ吸い付きます。 分岐の中で dUp > dDown(上へ伸ばすのか下へ伸ばすのか)も決めているので、 上方向フィル・左方向フィルも同じ1関数で扱えます。

2D(矩形タイル)だけは例外: 「縦にも横にも同時に広げてタイル状に敷き詰めたい」場合だけは、 軸スナップを切って選択とポインタを対角とする矩形を返します。TssGrid では fillDirection:'both' のときに Alt を押しながらドラッグで発動―― 誤爆を避けるため、わざと「明示の操作」にしてあります。

解法②:連番か、コピーか――値から推測する

埋める矩形が決まったら、次は中身です。ここが Excel らしさの本体。判断はシンプルで、

  • 元の値がすべて数値で、2つ以上あり、差が一定(等差)なら → 連番として外挿(10,2030,40,50)。
  • 数値だけど等差でない/1つだけ → 既定はコピー
  • 文字や日付混じりなら → 元のパターンを周期で繰り返すA,BA,B,A,B)。
lineFill(srcVals, count, ctrl) {
  const nums = srcVals.map(toNum), numeric = nums.every(n => n !== null);
  let isSeries = false, step = 0;
  if (numeric && srcVals.length >= 2) {   // 等差かどうか調べる
    step = nums[1] - nums[0]; isSeries = true;
    for (let i = 2; i < nums.length; i++)
      if (nums[i] - nums[i - 1] !== step) { isSeries = false; break; }
  }
  // Ctrl は意味を反転:連番上では「コピー」、ただの数値上では「強制連番(+1)」
  if (numeric) isSeries = isSeries ? !ctrl : (ctrl ? (step = 1, true) : false);
  const out = [];
  if (isSeries) { const last = nums[nums.length - 1];
    for (let k = 1; k <= count; k++) out.push(fmt(last + step * k)); }
  else for (let k = 0; k < count; k++) out.push(srcVals[k % srcVals.length]); // 周期コピー
  return out;
}

ポイントは Ctrl の扱いです。Excel と同じく、Ctrl は意味を反転させます。 連番になる組み合わせの上で Ctrl を押せば「連番化せずそのままコピー」、 逆に 5 という1つの数値の上で Ctrl を押せば「6,7,8… と強制連番」。 修飾キー1つで“ユーザーが今ほしい方”に倒せるのが、地味ですが効きます。

解法③:タイルフィルは「剰余」で素直に書く

斜めに広げる 2D フィルは、元矩形をタイルのように敷き詰めるだけ。 負のインデックスにも耐えるよう剰余を正規化しておけば、上方向・左方向にも同じ式で効きます。

// 元の高さ sH・幅 sW のブロックを、target 全域にタイル配置
const sr = src.r0 + (((r - src.r0) % sH) + sH) % sH;
const sc = src.c0 + (((c - src.c0) % sW) + sW) % sW;
setCell(r, c, data[sr][sc]);   // (r,c) に対応する元セルを写す

いちばん効く設計:フィルは「1回の Undo」で戻る

どれだけ賢く連番を作っても、1回ドラッグした結果が Undo 1発で消えないと業務では使えません。 TssGrid では、フィルで書き換わる全セルを1つの配列にためて、たった1回だけ履歴に積みます

doFill(src, target, ctrl) {
  if (onBeforeAutofill(src, target) === false) return;  // 拒否できるフック
  const changes = [];                          // ← 変更を全部ここに集める
  if (vertical && horizontal) tileFill(src, target, changes);
  else if (vertical)         fillVertical(src, target, changes, ctrl);
  else                      fillHorizontal(src, target, changes, ctrl);
  pushCmd(changes, before, after, 'fill');   // ★ ここが唯一の“関所”=1コマンド=1Undo
  onAfterAutofill(src, target);
}

この pushCmd は、TssGrid のすべての値変更(手入力・貼り付け・削除・フィル)が必ず通る1つの関所です。 =フィルも特別扱いせず同じ通り道を使うので、onBeforeChange での検証も、 source:'fill' 付きの onAfterChange 通知も、Undo/Redo の粒度も、 全部タダで揃います。この「関所を1本に絞る」話は 別記事の IME 設計とも通じる、TssGrid の背骨です。

細部に宿るもの

  • ドラッグ中のプレビュー枠:確定前に「ここが塗られる」を薄い箱で見せる。中身は fillRectFromPointer を呼ぶだけ。
  • 隠し列はまたぐ:フィルタや非表示列があっても、見えている列だけに値を流す(隠し列は素通り)。
  • 表の下までドラッグで行追加autoInsertRow を付けると、ハンドルを最下行より下へ落としたぶんだけ行が増える(伝票入力で効く)。
  • 画面外まで伸ばせる:端へドラッグするとビューが自動スクロールし、窓の外のセルもモデル幾何から逆算して塗る。仮想スクロールとも両立。

触ってみる

下のグリッドで試せます。セルの選択枠の右下にある小さな四角(フィルハンドル)を掴んでドラッグしてください。

  • 「数量」列で 1020 の2セルを選び、下へ引っぱる30, 40, 50… と連番。
  • 「担当」列の1セルを下へ引っぱる → 同じ値がコピー。
  • 連番を引っぱるとき Ctrl を押しっぱなし → 連番化せずコピーに切り替わる(逆も同じ)。
フィルハンドルは「Excel の小技」に見えて、実は幾何(どこを)系列推測(何を)履歴(どう戻す)が交わる、グリッドの設計力が出る場所です。 この3つをそれぞれ素直な小さな関数に割り、値変更を1本の関所に通す―― それだけで、依存ゼロ・コア数十行のまま、毎日触っても違和感のないフィルになります。