npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@threestarsoftware/tssgrid

v0.1.31

Published

軽量・ノービルド・日本語IME堅牢な編集グリッド(vanilla JS / 依存なし / MIT)。Excel ライクな入力画面を <script> タグ1本で。

Readme

TssGrid

軽量・ノービルド・日本語IMEに強い編集グリッド(vanilla JS / 依存なし / MIT)。

Excel ライクなセル入力・範囲選択・フィル(連番/2D)・コピペ(Excel相互)・Undo/Redo を、 バンドラ無し・<script> 一発で使える小さなコンポーネントとして提供する。

なぜ作ったか

無料の Web グリッドは「日本語IMEが弱い」「商用は有料化」「中国製で採用しづらい」のどれかに当たりがち。 TssGrid は IME堅牢 × ノービルド × MIT を満たすことを目的にしている。

主要グリッド(Handsontable / AG Grid / jspreadsheet CE / SpreadJS)との機能比較と立ち位置は → examples/compare.html(他社が強い領域=TssGridの伸びしろも正直に併記)。

導入事例(Showcase)— 使ってる方へ 📣

TssGrid を使っている会社・ブランドを examples/showcase.html に無料・承認制で掲載します(個人・小規模も歓迎)。掲載するとサイトURLへリンク(被リンク)し、相互に宣伝になります。

  • 申請: 導入事例の掲載申請(GitHub Issue) から、会社名+ロゴ(PNG/JPG・200KB目安)を送るだけ。内容を確認のうえ承認・掲載します(スパム防止のため承認制)。
  • バッジ: 採用いただいた方は Powered by TssGrid バッジを自社サイトに貼っていただけると嬉しいです(任意)。

仕組み: 申請(Issue) → メンテナが showcase/adopters.js に追記(=承認)→ ページに表示。サーバー不要・GitHub だけで完結。

特長

  • IME堅牢: アクティブセルに常時フォーカスした本物の <input> を重ねる方式。「あ」が「a」に化けない / 変換確定の Enter でセルが飛ばない。
  • ノービルド: 素の HTML/JS。file:// で直接開ける(外部Excelからの貼り付けのみ http 推奨。後述)。
  • 依存なし / MIT: 商用利用可。
  • 範囲選択(ドラッグ / Shift+矢印 / Shift+クリック)、フィルハンドル(連番 / 2D矩形)、Excel相互コピペ、Undo/Redo。
  • Undo を上位で共有: HistoryManager を外出しにし、複数グリッドへ同じインスタンスを注入すると 1本の Undo タイムラインになる。

クイックスタート

<link rel="stylesheet" href="src/tssgrid.css">
<div id="grid"></div>
<script src="src/tssgrid.js"></script>
<script>
  const grid = new TssGrid(document.getElementById('grid'), {
    headers: ['名前', 'よみ', '数値'],
    data: [['彩瀬', 'あやせ', '1'], ['', '', '2']],
  });
  // 値の取得
  // grid.getData()  -> string[][]
</script>

npm / バンドラ(Vite・Webpack・Next・Nuxt・Angular …):

npm i @threestarsoftware/tssgrid
import { TssGrid, HistoryManager } from '@threestarsoftware/tssgrid';
import '@threestarsoftware/tssgrid/tssgrid.css';
// プラグインは個別に: import '@threestarsoftware/tssgrid/plugins/tss-merge.js'

型定義 (.d.ts) 同梱なので TypeScript ではそのまま補完が効きます(APIリファレンス → tssgrid…/api)。

プラグインの型(TypeScript): エディタ系プラグインは .d.ts 同梱で import するだけで型が効きます(.js 付きで import):

import TssCombo from '@threestarsoftware/tssgrid/plugins/tss-combo.js';   // ← 型が効く
// 対応: tss-combo / tss-autocomplete / tss-cascade / tss-calendar / tss-datetime / tss-time / tss-comment
columns: [{ data: 'pay', editor: TssCombo({ options: [{ code: '01', label: '現金' }], other: { code: '99', label: 'その他' }, codeField: 'payCode' }) }];

それ以外のプラグイン(usePlugin 系・format ヘルパ系)は型定義がまだ無いので、必要なら5行のアンビエント宣言で補えます(editor.open/close を持つ戻り値は EditorDef):

import type { EditorDef } from '@threestarsoftware/tssgrid';
declare function TssXxx(opts: any): EditorDef;   // 未同梱プラグインの暫定宣言

Node / CommonJS:

const { TssGrid, HistoryManager } = require('@threestarsoftware/tssgrid');

(UMD。<script> 読み込みでは window.TssGrid / window.HistoryManager。CDN は https://cdn.jsdelivr.net/npm/@threestarsoftware/tssgrid。)

フレームワークで使う(React / Vue / Angular)

専用ラッパーは作りません。vanilla on DOM なので CRA/Vite/Next/Webpack/Angular/Nuxt でそのまま動きます。コツは マウント時に生成し、アンマウント時に grid.destroy() するだけ(リスナ・ResizeObserver・DOM を後始末。idempotent)。

React:

import { useEffect, useRef } from 'react';
import { TssGrid } from '@threestarsoftware/tssgrid';
import '@threestarsoftware/tssgrid/tssgrid.css';

function Grid({ columns, data }) {
  const ref = useRef(null);
  useEffect(() => {
    const grid = new TssGrid(ref.current, { columns, data });
    return () => grid.destroy();        // 後始末
  }, []);
  return <div ref={ref} />;
}

Vue 3:

<script setup>
import { onMounted, onBeforeUnmount, ref } from 'vue';
import { TssGrid } from '@threestarsoftware/tssgrid';
import '@threestarsoftware/tssgrid/tssgrid.css';
const el = ref(null); let grid;
onMounted(() => { grid = new TssGrid(el.value, { columns, data }); });
onBeforeUnmount(() => grid?.destroy());
</script>
<template><div ref="el"></div></template>

Angular:

import { Component, ElementRef, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
import { TssGrid } from '@threestarsoftware/tssgrid';

@Component({ selector: 'app-grid', template: '<div #host></div>' })
export class GridComponent implements AfterViewInit, OnDestroy {
  @ViewChild('host') host!: ElementRef;
  private grid!: TssGrid;
  ngAfterViewInit() { this.grid = new TssGrid(this.host.nativeElement, { columns, data }); }
  ngOnDestroy() { this.grid.destroy(); }
}

複数グリッドで Undo を共有

const history = new HistoryManager();
new TssGrid(elA, { name: '甲', history, headers, data });
new TssGrid(elB, { name: '乙', history, headers, data });
// 甲乙どちらで編集しても、Ctrl+Z は「やった順の逆」で戻り、該当グリッドが自動でフォーカスされる。
// history.undo() / history.redo() を上位の Undo ボタンに繋げてもよい。

history を渡さなければグリッドが自前で HistoryManager を生成する(単体動作。コードは同じ)。

データの持ち方(配列 / オブジェクト配列)

data は 2次元配列(string[][])でも オブジェクト配列(object[])でも渡せます(自動判別)。列にキーを紐付けると、列名でアクセスできます。

const grid = new TssGrid(el, {
  columns: [
    { data: '品番' },                    // ← columns[c].data で列にキーを紐付け
    { data: '品名' },
    { data: '数量', type: 'number', thousands: true },
  ],
  data: [
    { 品番: 'P-100', 品名: 'ボルト', 数量: '1200', id: 42 },  // ← オブジェクト配列で渡せる
    { 品番: 'P-101', 品名: 'ナット', 数量: '1200', id: 43 },
  ],
});

grid.getValue(0, '品番');     // → 'P-100'(列名 or 列index でアクセス)
grid.setValue(0, '数量', '5'); // → 列名で書き込み(Undo 可)
grid.getRow(0);               // → {品番:'P-100', 品名:'ボルト', 数量:'1200', id:42}(1行を横断)
grid.getColumn('品番');       // → ['P-100','P-101',...](getRow の対。1列を縦断)
grid.getRows();               // → [{品番:'P-100', ...}, ...](全行をオブジェクト配列で)
grid.getColumns();            // → {品番:['P-100',...], 品名:[...], 数量:[...]}(列ごとの値配列)

grid.toCSV();                 // → CSV文字列(RFC4180クォート / 既定は素の値・空行除外)
grid.downloadCSV('在庫.csv'); // → UTF-8 BOM付きで即ダウンロード(Excel日本語が文字化けしない)
// JSON出力は専用APIなし= JSON.stringify(grid.getRows()) で足ります(JSONは標準、CSVは非標準なので内蔵)
  • CSV出力: toCSV(opts) / downloadCSV(filename, opts)。opts: headers(既定true) / formatted(既定false=素の値で数値はそのまま, trueで表示形 ¥1,234) / skipEmpty(既定true=空行除外) / eol(既定 '\r\n') / sanitizeFormula(既定true)。"/カンマ/改行を含むセルは自動でクォート。CSV数式インジェクション対策も既定ON==/@/+/- 始まりの非数値セルを ' で無害化(Excel/Sheetsで式として実行されない。-100 等の数値は保全)。生値が要るなら toCSV({sanitizeFormula:false})。CSV入力(読込)は非内蔵=Excelからの貼り付けで取り込めるため(パースが必要なら PapaParse 等を)。

  • Excel(xlsx)出力: コア非搭載(zipバイナリで重いため)。getRows() + ExcelJS(MIT, CDN+SRI)でサンプル提供。数値を数値のまま・太字ヘッダ・¥#,##0書式・ヘッダ行固定まで付けられます → 動く例: examples/excel-export.html。

  • Excel(xlsx)読込: 同じく ExcelJS でサンプル提供。データ(セルの値)のみ取り込み(書式・数式は無視=数式は結果値、日付は ISO)。1行目を見出し、2行目以降を setData に → 動く例: examples/excel-import.html(外部ファイル不要の「生成→読込」round-trip 付き)。CSV と違い ExcelJS がパースを担うので自前実装は不要。

  • CSV読込: コア非搭載(簡単な取り込みは Excel からの貼り付けで足りるため)。PapaParse(MIT, CDN+SRI)でサンプル提供。素朴な split(',') では壊れるクォート内カンマ・改行・"" エスケープ(RFC4180)を正しく解き、文字コード指定(Excel の Shift_JIS な .csv も読める)・大容量のストリーミング/Workerまで対応。1行目を見出し、2行目以降を setData に → 動く例: examples/csv-import.html(クォート/改行入りの「生成→読込」round-trip + ファイル D&D・文字コード切替付き)。

  • 保存値は常に文字列の2次元配列を内部に持ち、描画・編集・コピペ・履歴はすべて従来どおり。オブジェクト配列は入出力時の変換層です(大きな作りは変えていません)。

  • getData() は常に2次元配列を返す(後方互換)。getRows() はオブジェクト配列で返す。

  • 画面に出さないフィールド(上の id など)も getRows() で保持されます(元オブジェクトを内部に退避。編集・行挿入・削除をしても維持)。

  • 見出し headers を省略すると、columns[c].title(あれば)→ data キー → A,B,C… の順で補完します。

  • getValue / setValue の列指定は 数値index でも data キー文字列 でも可。setData(rows) も配列/オブジェクトを自動判別。

  • 配列形式しか使わない場合は今までどおり。getRows() は data キーのある列だけを対象にします。

  • ネストパス対応: columns[c].data は 'name.last' のようなドット区切りも指定できます(Handsontable 互換)。

new TssGrid(el, {
  columns: [{ data: 'age' }, { data: 'name.last' }, { data: 'name.first' }],
  data: [{ name: { first: '佐藤', last: '一郎' }, age: 25 }, /* … */ ],
});
grid.getValue(0, 'name.last');   // → '一郎'
grid.getRows()[0];               // → { name: { first:'佐藤', last:'一郎' }, age:'25' }(入れ子で復元、非バインドの入れ子フィールドも保持)

整列 / セル単位の読み取り専用(実行時)

// 整列(range 省略時は現在の選択範囲)。'left'|'center'|'right'|'top'|'middle'|'bottom'|{h,v}|'reset'
grid.setAlignment('center');
grid.setAlignment('right', { r0: 0, c0: 2, r1: 5, c1: 2 });
grid.getAlignment(0, 2);            // → { h:'right', v:null }
new TssGrid(el, { columns: [{ align: 'center', valign: 'middle' }] });   // 列既定

// セル単位の読み取り専用(実行時トグル。列の readOnly とは別に効く)
grid.setCellReadOnly(true);                                   // 選択範囲を読取専用に
grid.setCellReadOnly(false, { r0: 1, c0: 1, r1: 1, c1: 1 });  // 解除
  • 整列はセル上書き>列既定(columns[c].align/valign)の順。数値列の既定右寄せも上書きできます。
  • どちらも列の挿入・削除でセルに自動追従(Undo も整合)。setData で新データを入れるとクリアされます。
  • setAlignment/setCellReadOnly 自体は Undo に積みません(書式・状態のため。setColWidth と同様)。

並べ替え(ソート)

コアにソートUIは持たせていません(業務フォーム志向)。並べ替えは数行で外から組めます。

grid.sortBy('数量', 'asc');        // 列キー or index で並べ替え(数値っぽければ数値比較)
grid.sortBy(2, 'desc');
grid.sortByCols([                  // ★ 多段(複数列): 先頭=第1キー。同値のときだけ次段で決める
  { col: '部署', dir: 'asc'  },
  { col: '売上', dir: 'desc' },
]);
grid.sortRows((a, b) => a.qty - b.qty);   // 任意比較(行はオブジェクト/配列)
  • sortBy/sortByCols/sortRows はデータ実体を並べ替えます。整列・セル readOnly・checkbox・隠しフィールド(_src)は行ごと追従します。Undo には積みません(書式系と同じ割り切り)。
  • sortByCols の各段は sortBy と同じ比較規則(数値/文字列自動判定)。安定ソートなので、全段が同値の行は元の並びを保ちます。**多段ソートのヘッダUI(▲▼アイコン+優先度番号・本体クリックで列選択と両立)**は onHeaderRender を使った → 動く例: examples/multisort.html。
  • 未入力(空)行はソート対象外=元の順序のまま最下部に固定(minSpareRows の追加用行が上に来ない)。
  • clearSort() で初期状態(入力順)に戻せます(isSorted() で判定)。行参照ベースなので編集してもズレず、ソート後に追加した行は末尾へ、削除済みは無視。

ヘッダクリックで並べ替え(▲▼インジケータ付き)も外部で組めます(→ 動く例: examples/sort.html):

let sortCol = -1, dir = 'asc';
const base = ['品番', '数量', '単価'];
new TssGrid(el, {
  headers: base.slice(),
  onHeaderClick(c) {                                  // ヘッダクリックを横取り
    dir = (sortCol === c && dir === 'asc') ? 'desc' : 'asc'; sortCol = c;
    grid.headers = base.map((h, i) => i === c ? h + (dir === 'asc' ? ' ▲' : ' ▼') : h);  // ▲▼ は文字を足すだけ
    grid.sortBy(c, dir);
    return false;                                     // 既定の列選択は抑止
  },
});

行のドラッグ移動(rowReorder)

rowReorder: true で行ヘッダ左に「つまみ」⠿が出て、ドラッグで行を並べ替えられます(ドロップ位置に緑のガイド線)。データ実体ごと動くので値・整列・readOnly・隠しフィールドも追従、行番号は自動で振り直し(→ 動く例: examples/row-reorder.html)。複数行は行ヘッダで範囲選択(クリック→Shift+クリック)してから選択内のつまみを掴めばまとめて移動します。

new TssGrid(el, {
  rowReorder: true,                              // 行ヘッダにつまみを表示
  onAfterRowMove: (from, to, count) => { /* … */ },// 確定後(from=元の先頭, to=新しい先頭, count=行数)
});
grid.moveRow(from, to);                           // 1行(to は 0..ROWS の挿入境界)
grid.moveRows(r0, r1, to);                        // 連続する複数行をまとめて移動
  • 行移動はUndo/Redo 対応(Ctrl+Z/Ctrl+Y。意図的な編集なので戻せる。※sortBy の並べ替えは履歴非対象でこちらと別)。
  • minSpareRows と併用すると追加用の空行が末尾以外に動くことがあるので、入力途中の表では rowReorder を切るか並べ替え確定後に使うのが無難。

行削除アイコン(rowDelete)

rowDelete: true で右端に専用の「削除列」が出て、各行の 🗑 を1クリックで当該行を削除できます(右クリック→「行を削除」と同一経路)。削除はUndo/Redo 対応(Ctrl+Z で復活)なので確認ダイアログは出しません。非技術者にも分かる削除アフォーダンスとして、右クリックに気づかなくても消せます(→ 動く例: examples/row-delete.html)。

new TssGrid(el, {
  rowDelete: true,           // 右端に🗑削除列(既定 false)
  rowDeleteWidth: 36,        // 削除列の幅(px)・任意(既定 36)
  rowReorder: 'header',      // 併用可: 左=⠿で移動 / 右=🗑で削除(役割が左右で分かれる)
});
  • 削除列は非データのシステム列です。getData/getRows/toCSV・列インデックス・列選択・リサイズ・並べ替えの対象外で、データ列とは混ざりません。データ列の右隣に並ぶ末尾列で、横幅がはみ出す表では横スクロールで表示されます(表がはみ出す時に🗑を貼り付けると、選択枠やデータ列と重なるため貼り付けません)。
  • 最後の1行(0行にはできない)で🗑を押すと、**削除ではなく「中身クリア(行は残す)」**になります=🗑は無効にならず、行の内容だけ空に(readOnly セルは保持・1 undo で復活。ツールチップも「内容をクリア」に変わります)。「消してから空行を足す」のと同じ結果を、0行に一瞬も落とさず1手で。
  • allowDeleteRows: false なら削除列そのものを出しません。minRows を 2 以上に設定した場合は下限で🗑を淡色の無効表示にします(そのロック行は削除もクリアもしません)。
  • 込み入った削除(一括・条件付きなど)は、削除列に頼らずチェックボックス列+「選択行を削除」ボタンで組むのが素直です(🗑は「1行1クリック」の単機能に留めています)。

ブラウザ自動入力の抑止(suppressAutofill)

グリッドは編集用に本物の <input> を重ねる方式なので、稀に Chrome の自動入力(「ポイントカードを管理…」等) がセル編集時に被さることがあります。既定で name + autocomplete="off" +各パスワードマネージャの無視属性を付けて抑止していますが、Chrome はポイントカード/住所/支払い系で autocomplete="off" を無視することがあり、特に周囲に自前のラベル付き <input>(フォーム塊)を並べた画面(モーダルのヘッダ入力欄など)だと Chrome がそれをフォームと認識して誘発します。

その場合は suppressAutofill: true を渡すと、共有エディタの autocomplete を new-password に切り替えて確実に止めます(Chrome が off を無視する系にも効く唯一の実用手)。

new TssGrid(el, { suppressAutofill: true });   // 既定 false
  • 副作用として環境により鍵アイコンや生成パスワードの提案が出ることがあるため opt-in(既定は付けません)。値はマスクされません(type は text のまま)。
  • あわせて、あなたのページ側の素の <input>(テーブル名欄など)にも同じ autocomplete="new-password" を付けてください。誘発の主因はグリッドではなく周囲のフォーム塊のことが多いためです。

依存ドロップダウン(options を関数に=行ごとにリスト差替え)

columns[c].options は配列(列共通)のほか、関数 (r, row) => [...] を渡せます。すると行ごとに違う候補になり、「A 列で選んだ値に応じて B 列のリストが変わる」依存ドロップダウン(都道府県→市区町村 等)が作れます。row はその行の値配列(grid.data[r])。開くたびに options を読み直すので、列共有の静的配列では両立できない「行ごとに別リスト」が同時に成立します(→ 動く例: examples/dependent-dropdown.html)。

const CITY = { 東京都:['渋谷区','新宿区'], 大阪府:['大阪市','堺市'] };
columns: [
  { type:'dropdown', options: Object.keys(CITY) },          // 親(静的)
  { type:'dropdown', options: (r, row) => CITY[row[0]] || [] }, // 子(行依存)
]
// 親が変わったら子をクリア(古い値が残らないように)
onAfterChange: (chs) => chs.forEach(ch => { if (ch.c === 0 && ch.oldValue !== ch.newValue) grid.setValue(ch.r, 1, ''); });
  • 行依存でも表示ラベル・検証(保存値が候補にあるか)はその行の候補で解決します。静的配列は従来どおり(後方互換)。

セル内ボタン(html:true + クリック委譲)

専用のボタン列 API はありません。columns[c].html:true + format が返す HTML でセルにボタンを描き、grid.table に click 委譲して data-* で拾うのが定石です。行の状態でボタンを出し分けたいときは、状態を列に持たせて format の**第3引数 row(その行の値配列)**から読みます(→ 動く例: examples/cell-button.html=未申請/申請中/編集中でボタン切替)。

// format の引数は (value, { r, c }, row)。row = その行の値配列(grid.data[r])
columns: [ /* …, 状態列(index 2), … */,
  { html:true, readOnly:true, format: (_v, _cell, row) => {
    const st = row[2];                       // 状態を「行の値配列」から読む(grid は参照しない)
    if (st === '編集中') return '<button class="mybtn" data-act="save">保存</button>';
    if (st === '申請中') return '<button class="mybtn" data-act="edit">編集</button>';
    return '<button class="mybtn" data-act="submit">申請</button>';
  } } ]
grid.table.addEventListener('click', e => {
  const b = e.target.closest('.mybtn'); if (!b) return;
  const r = +b.closest('tr[data-r]').dataset.r;
  grid.setValue(r, '状態', '申請中', true);   // 状態遷移(readOnly列は force=true)
  grid.redraw();                             // ← format 再実行でボタンが変わる
});
  • ボタン列は readOnly:true にして編集モードに入らないように。行番号は closest('tr[data-r]') から。空セルでも format は走ります(飾り列を全行に描ける)。
  • ⚠️ 初回描画は new TssGrid(...) の構築中に走るので、format の中で grid 変数を参照しない(まだ未代入=TDZ で例外)。状態は第3引数 row から読むのが安全(grid.getValue を使いたくなるが初回で落ちる)。状態を変えたら grid.redraw() で format を再実行してボタンを更新します。
  • html:true はエスケープを外すので、ラベルにユーザー入力を入れる時は自分でエスケープを(XSS 責任は HTML 提供側)。

非破壊フィルタ(filter / clearFilter)

コア内蔵の非破壊フィルタ(行インデックスマッパ)。全行は内部の _allRows(マスタ)に保持したまま、絞り込んだ可視部分集合だけを描画します。要点は絞り込み後の描画・ライブ検索が軽いこと(自社調べ:10万行を保持して約200行に絞り込み → 初回描画 ≈ 0.5 秒、絞り込み済みのライブ検索 ≈ 数 ms)→ 動く例: examples/filter-live.html。 なお全件をそのままスクロール表示したい大量行は、仮想スクロール virtual: true を使います(下記)。

grid.filter(row => row[2] >= 100);     // 述語: (row, masterIndex, src) => bool
grid.filter({ 部署: '営業' });          // 簡易形: {列キー or 見出し名: 値|関数}(AND)
grid.clearFilter();                    // 解除=全行へ(フィルタ中の編集も残る=非破壊)
grid.isFiltered();                     // 真偽
grid.getAllRows();                     // マスタ全行(フィルタ中でも全件)
// onFilter: ({count, total, cleared}) => {...}
  • 非破壊: フィルタ中に編集した値も、行参照の共有でマスタへ自動反映。clearFilter() で全件+編集が残る。rowH/整列/セル readOnly も非破壊に往復。
  • フィルタ中の行挿入/削除OK(マスタへ追従+Undo対応/同一フィルタ内)。ソートはビュー内で有効。
  • 選択/ナビ/結合/幾何はビューのまま無改変=フィルタ未使用なら一切コスト無し。
  • 制限(v1): フィルタ中は 列の挿入削除・行移動・セル結合はロック(解除してから)。filter/clearFilter はUndo履歴をクリア(ビューindex基準の履歴が境界を跨ぐと不整合になるため)。
  • フィルタUI(ヘッダの絞り込み等)はコアに入れずプラグイン tss-filter(下記)で。素朴な setData 差し替え方式の旧サンプルは examples/filter.html。

仮想スクロール(virtual: true)

大量行を全件そのままスクロール表示したい時に。全行は data に保持したまま、画面に見える窓ぶん+バッファだけを描画します(固定行高ウィンドウイング)。全件を一度に DOM 化しないので初期描画が一瞬・スクロールも軽い。有効化はオプション一行だけ。

new TssGrid(el, { virtual: true, data, /* … */ });
new TssGrid(el, { virtual: { buffer: 10 }, data });   // 窓の上下バッファ行数(既定6)
  • 自社調べ(headless Chrome / Win11): 10 万行で初回描画 ≈ 46 ms(全件描画 ≈ 10.5 秒に対して)。スクロール 1 コマの窓差し替え ≈ 3 ms。動く例: examples/virtual.html。
  • 制約(v1): 行高一定/固定行列・折り返し・セル結合は非対応(指定時は自動で無効化+警告)。行番号の桁数が増える場合は rowHeaderWidth で行ヘッダ幅を広げられます。
  • フィルタとの関係: filter() は「絞り込んだ部分集合だけ描く」軽さ、virtual は「絞らず全件をスクロールする」軽さ。用途で使い分け(v1 では併用は未対応)。

ページネーション(サーバ側ページング)

ページネーションはサーバの API 形(offset/limit・total 件数の有無)に依存するので、TssGrid 本体は「渡された1ページ分を描く」だけに徹します(コアに作り込みません)。ロジックはアプリ側で、ページ変更 → fetch → grid.setData(pageRows)、ページ番号ボタンや「N 件中 M〜」はグリッドの外に置きます。動く例: examples/pagination.html。

async function load() {
  const { rows, total } = await fetchPage((page - 1) * size, size);  // サーバの offset/limit
  grid.setData(rows);          // ← TssGrid は1ページ分を描くだけ
  renderCount(total);          // 「N 件中 M〜」はグリッドの外で
}
// 前/次/ページ番号は <button> で外に置き、go(p) → load() を呼ぶ
  • total が返るサーバ: 「全何件・何ページ」まで出せる(上の例)。total が無い(cursor のみ): ページ番号は出せないので appendRows で「もっと読む」=無限スクロール型に。
  • 1本の長いスクロールで必要分だけ取得したいなら、ページ送りでなく setRowCount(total) + onViewportChange(start,end) + fillRows(start, rows)(AG Grid の Infinite Row Model 相当・virtual:true 前提/未取得行は「読込中」表示)。動く例: examples/lazy-load.html(10万件を見える範囲だけ取得)。
  • sort/filter をサーバ側でやる場合も同じ=操作 → 再 fetch → setData(遅延スクロール型なら onSortFilterChange で委譲)。

同梱プラグイン: ヘッダ オートフィルタ(filter UI)

plugins/tss-filter.js+.css= 各見出しのロート(漏斗)アイコンをクリックで値チェックリスト+検索のポップアップ(Excel風。ソートの▲▼と区別できるアイコン)。複数列の条件は AND。中身は上のコア filter() を呼ぶだけ。動く例: examples/filter-ui.html。

<link rel="stylesheet" href="plugins/tss-filter.css">
<script src="plugins/tss-filter.js"></script>
plugins: [ TssFilter.plugin() ]                 // 全列にフィルタUI
plugins: [ TssFilter.plugin({ columns:['部署', 2] }) ]   // 対象列を限定
grid.getPlugin('filter').clearAll();            // 全条件クリア
  • 値候補はマスタ全行から集計(絞り込み中でも全値が出る)。フィルタ済み列に印。値は HTML エスケープ。

同梱プラグイン: 共有マスタ(複数グリッドで1データ)

plugins/tss-shared.js= 1つのマスタ(同じ行データ)を複数グリッドで共有。左右を別フィルタ/別ソートで表示でき、片方の編集がもう片方の同じ行へ即反映(行参照を共有)。A系/B系を並べて突き合わせ編集する用途。動く例: examples/shared-master.html。

<script src="plugins/tss-shared.js"></script>
const shared = TssShared.create(masterRows, { headers:[...], columns:[...], rowHeaders:true });
const gridA = shared.attach(hostA, { plugins:[TssFilter.plugin()] });
const gridB = shared.attach(hostB, { plugins:[TssFilter.plugin()] });
gridA.filter({ 部署:'営業' });  gridB.filter({ 勤務地:'東京' });   // 各々別フィルタ
gridA.insertRow(0); gridA.deleteRows(2);   // 行の挿入/削除も両ビュー&マスタへ伝播
shared.getMaster();                         // マスタ全行(getRows 形式)。 shared.detach(gridA);
  • 値編集は行参照共有で即反映。行/列の挿入削除も伝播(構造変更後は他ビューを master から再構成=フィルタは再適用・そのビューのソートはリセット)。
  • 配列データ/オブジェクト配列データ どちらも対応(オブジェクト時は非表示フィールドも保持)。minSpareRows:0 推奨。
  • 連携に 同期スクロール を足せば「並べて比較+同時スクロール+同時編集」に。

ショートカット / プラグイン / 破棄

カスタムショートカット(軽量 shortcut-manager 相当。context は 'grid'=ナビ / 'editor'=編集中 / 'all'):

new TssGrid(el, {
  shortcuts: [
    { keys: 'Alt+D', name: 'dup', context: 'grid', handler: (e, { grid }) => grid.insertRow(grid.active.r, 'below') },
    { keys: ['Ctrl+S', 'Ctrl+Shift+S'], handler: () => save() },   // 複数キー可
  ],
});
grid.addShortcut({ keys: 'Ctrl+K', handler: () => {} });   // 実行時に追加
grid.removeShortcut('dup');
  • マッチすると既定動作を抑止します(handler が true を返すと既定も続行)。cmd/meta は Ctrl 扱い。
  • 全キーをまとめて捌きたいなら onBeforeKeyDown(e)(false / preventDefault() でグリッド既定を止める)。

組込ショートカット(Excel 互換・既定 ON): 業務入力でよく使うキーを最初から用意(すべて未バインドキー=追加的)。

| キー | 動作 | 備考 | |---|---|---| | Ctrl+; | 今日の日付(YYYY-MM-DD)をセルへ | date 列にそのまま入る | | Ctrl+: | 現在時刻(HH:MM)をセルへ | JIS の : 専用キーも US の Ctrl+Shift+; も両対応(打鍵結果の文字で判定) | | Ctrl+D | 上のセルをコピー(fill down)| 範囲選択なら先頭行を下へ一括 | | Ctrl+R | 左のセルをコピー(fill right)| 範囲選択なら左端列を右へ一括 |

  • いずれも setValue/検証を通り Undo 1回で戻せます(fill は列の型に合わない値は貼り付けと同様に弾かれます)。readOnly セルはスキップ。
  • 無効化: dateShortcuts: false(Ctrl+;/Ctrl+:)・fillShortcuts: false(Ctrl+D/Ctrl+R)。独自に addShortcut で同じキーを登録すればそちらが優先されます。
  • Ctrl+;/Ctrl+: は date/time/カレンダー等のピッカーを開いている編集中でも効きます(ピッカーを閉じて今日/現在時刻を挿入。これによりキーがブラウザ既定に素通りするのも防止)。テキスト(自由入力)編集中は横取りしません。JIS で時刻は Ctrl+:(: 専用キー)を使ってください(Ctrl+Shift+; は JIS では + =ブラウザ拡大になります)。
  • 入れる形式: date/time 型の列は列の仕組みに従う(ISO 保存=YYYY-MM-DD/HH:MM、見た目は列の format で / でも自由)。それ以外(テキスト等)の列は todayFormat / nowFormat で形式を選べます(既定 'YYYY-MM-DD' / 'HH:MM'。トークン YYYY/MM/DD・HH/mm、または (d: Date) => string)。例: todayFormat: 'YYYY/MM/DD'(スラッシュ区切り)。

プラグイン(軽量 base-plugin 相当。init(grid)→{destroy?} の薄い契約):

const autoSavePlugin = (grid) => {
  const prev = grid.onAfterChange;
  grid.onAfterChange = (ch, src) => { prev && prev(ch, src); localStorage.setItem('grid', JSON.stringify(grid.getData())); };
  return { name: 'autosave', destroy() { grid.onAfterChange = prev; } };
};
new TssGrid(el, { plugins: [autoSavePlugin] });
grid.usePlugin(autoSavePlugin);                 // 実行時に追加
TssGrid.registerPlugin('autosave', autoSavePlugin);   // 名前登録 → plugins:['autosave'] で使える

破棄: SPA でグリッドを出し入れするときは grid.destroy() で document/window リスナ・ResizeObserver・プラグインを全解除し、DOM も除去します(リーク防止)。

カスタムエディタ

組込エディタ(text/dropdown/checkbox/date/time/number)以外に、独自の編集UIを列に差せます。columns[c].editor に薄いオブジェクト契約を渡すだけ。

const myEditor = {
  // 編集開始時に呼ばれる。UI を出して、確定で commit(値) / 取消で cancel() を呼ぶ。
  open({ grid, r, c, value, td, commit, cancel }) {
    const inp = document.createElement('input');
    inp.value = value;
    const box = td.getBoundingClientRect();
    Object.assign(inp.style, { position: 'fixed', left: box.left + 'px', top: box.top + 'px', width: box.width + 'px' });
    document.body.appendChild(inp); inp.focus();
    inp.onkeydown = e => { if (e.key === 'Enter') commit(inp.value); if (e.key === 'Escape') cancel(); };
    this._inp = inp;
  },
  close() { this._inp.remove(); },     // 任意。後片付け
};

new TssGrid(el, { columns: [{ editor: myEditor }] });
// editor は () => ({...}) のファクトリでも可(セルごとに生成したい時)
  • open が受け取る { grid, r, c, value, td, commit, cancel } だけ知っていれば作れます。F2 / ダブルクリック / 文字入力で開きます。
  • commit(value) は通常の確定経路(pushCmd)を通るので、validator/parse/format/読み取り専用がそのまま効き、Undo にも積まれます。
  • 編集中に別セルをクリックすると cancel() 相当で閉じます。描画(セルの見た目)は従来どおり format 等で制御。
  • ⚠️ 上記の(フォーカスを奪う)エディタは クリック / F2 で開く型です。打鍵の1文字目や IME 直打ちから始めたいなら次の inline を使ってください。

editor.inline: 打鍵・IME 直打ちを殺さないエディタ(サジェスト向け)

editor.inline = true を宣言すると、入力は本体の IME 堅牢な共有 input のままで、エディタは「候補を描く」役に徹します。=打った1文字目が残り、IME 直打ち(変換)も素のテキスト列と完全に同じに効きます。「セルに打ちながら候補を絞るサジェスト/ルックアップ」はこの型で作ります。

const suggest = {
  inline: true, icon: '🔍',                 // icon / openOnClick は inline でも使えます
  open(ctx) { /* ctx.input = 打鍵/IMEが入る共有input。ここに候補ポップアップを添える(フォーカスは奪わない) */ },
  onInput(value, ctx) {
    // 打つたびに呼ばれる=候補を絞る。値として解釈したいなら ctx.parseCell(=セルと同じ読み方。
    // 独自パーサを書くと本体と食い違う)。解釈できない打ちかけは null。
    const v = ctx.parseCell(value); if (v == null) return;
  },
  onKeyDown(e, ctx) {                       // 本体より先に受ける。IME 変換中も呼ばれる
    if (e.isComposing || e.keyCode === 229) return;   // ★変換中は手を引く(↑↓を奪わない)
    if (e.key === 'ArrowDown') { /* 候補を選ぶ */ return false; }   // false で本体既定を止める
    if (e.key === 'Enter') { ctx.commit(選んだ値); return false; }  // commit=関所を通る
  },
  close() { /* 候補を片付ける */ },
};
new TssGrid(el, { columns: [{ editor: suggest }] });
  • ctx は通常の {grid,r,c,value,td,commit,cancel} に加えて input(共有input)/ setValue(v)(候補で置換・確定はしない)/ parseCell(v) / inline:true が入ります。
  • 打った内容に追従するなら ctx.parseCell(v) を使ってください=セルの確定と同じ読み方(columns[c].parse があればそれ、無ければ型ごとの組込=date/time/number)で保存値に解釈し、解釈できなければ null(打ちかけ)。エディタが独自パーサを持つと「セルは受け付けるのに UI は追従しない」食い違いになります(実際 TssCalendar がこれを踏みました)。
  • 確定は本体の commit()=関所を通るので validator/parse/format/読み取り専用/Undo はそのまま効きます。
  • ポップアップ側は mousedown を preventDefault してフォーカスを奪わないこと(共有 input に打ち続けられる)。
  • 対象は text/number 系の列(dropdown/checkbox/ネイティブ日付は従来どおり対象外)。
  • 自作エディタの inline は既定 false(宣言しない限り挙動は一切変わりません)。共有 input に乗るには実装側の合意が要るためで、ここは opt-in が正しい既定です。
  • 同梱プラグインの inline は既定 true:TssAutocomplete(打ちながら候補を絞る)/TssCalendar・TssDatetime・TssTime(日付/時刻をそのまま直打ちできる。カレンダーは打った内容に追従してその日へジャンプ)。何も書かずにコピペした人が、看板どおりの「打ち始めれば書ける」を得ます。
    • 日付系は 「キー派は打つ/マウス派はクリック」 の割り切り=矢印キーは奪いません(文字カーソル/セル移動のまま)。伝票入力のように「6/20 と打つ方が速い」場面向け。
    • inline: false で旧型(ポップアップにフォーカスし ←→↑↓ で日移動)に戻せます。ただし セルにいきなり打ち始めて上書き(表計算で一番よく使う操作)ができなくなる=クリック/F2 で開いてから打つことになります。矢印での日送りが要る場面だけ選んでください。

同梱プラグイン: 休日カレンダー対応の日付ピッカー

plugins/tss-calendar.js= ネイティブ <input type=date> では出来ない会社の休日カレンダー(JSON)対応の日付ピッカー。勤怠・シフト・予約などで効きます(動く例: examples/calendar.html)。

<link rel="stylesheet" href="plugins/tss-calendar.css">
<script src="plugins/tss-calendar.js"></script>
const cal = TssCalendar({
  holidays: { '2026-01-01': '元日', '2026-08-13': { name: '夏季休業', type: 'company' } },  // 日付→名称
  weekend: [0, 6],            // 既定。土日を色分け
  // min, max, disable:['holiday','weekend'](選択不可), weekLabels, monthLabel …
});
new TssGrid(el, { columns: [{ type: 'date', editor: cal, format: 'yyyy/mm/dd' }] });
  • セル右端に 📅 が出てシングルクリックで開閉(再クリックで閉じる=トグル)、開いている間もフィルハンドルで下へコピー可(ドロップダウンと同じ操作感)。年・月はプルダウンで直接ジャンプ。祝日は赤+名称ツールチップ、土日は色分け。保存値は ISO のまま。
  • 打つ人はそのまま打てます(既定 inline): セルに 6/20 と打ち始めればそのまま編集開始(1文字目も落ちない・IME 直打ちも効く)、カレンダーは打った日へ追従。確定は Enter か日をクリック。矢印はセル移動のまま(カレンダーは奪いません)。
    • 矢印/PageUp・Down でカレンダー内を移動する旧型が要るなら inline: false(→ editor.inline)。セルに直接打ち始める操作は失われます。
  • 見た目は CSS 変数(--cal-accent など)と tss-cal-* クラスで自由に(依存ゼロ)。グリッド外でも cal.openAt(anchorEl, isoValue, onPick) で単体利用可。
  • opts: icon(既定 '📅', '' で消す) / openOnClick(既定 true) / yearMin・yearMax(年プルダウンの範囲) も。
  • 仕組み: カスタムエディタが icon / openOnClick を申告すると、グリッドが右端アイコン表示+シングルクリック起動を行います(自作エディタでも同様に使えます)。

同梱プラグイン: 時刻ピッカー(時/分プルダウン)

plugins/tss-time.js= 業務の時刻入力向けの時/分プルダウンピッカー(クロックフェイスのようなリッチさは省き、任意時刻を素早く)。保存値は常に 24h 'HH:MM'(組込 type:'time' と互換)。動く例: examples/time.html。

<link rel="stylesheet" href="plugins/tss-time.css">
<script src="plugins/tss-time.js"></script>
const t = TssTime({ step: 15, hour12: false });   // 15分刻み / 24h
new TssGrid(el, { columns: [{ type: 'time', editor: t }] });
  • opts: step(分刻み, 既定1) / hour12(AM/PM 表示, 既定false) / icon(既定 '🕐') / openOnClick(既定true) / className。
  • 🕐 をシングルクリックで開閉、現在/決定ボタン、キーボード(Enter 決定 / Esc 取消)、外側クリックで採用、フィルで下へコピー。見た目は --time-* 変数と tss-time-* クラスで自由に。

同梱プラグイン: セルのコメント(tss-comment・Excel のメモ風)

plugins/tss-comment.js= セルに注釈を付ける。コメントのあるセルは右上に赤い三角、PC はホバー・スマホはタップで吹き出し、右クリック or Shift+F2 で挿入・編集・削除。動く例: examples/comment.html。

★他プラグインと干渉しません:セルのフック(cellClass/cellStyle/editor/format)を一切奪わず、コメント済みの <td> に自前でクラスを足すだけ(再描画追従は MutationObserver)。条件付き書式・heatmap・cascade/calendar と同じセルで共存できます。

<link rel="stylesheet" href="plugins/tss-comment.css">
<script src="plugins/tss-comment.js"></script>
const menu = [];                                   // contextMenu は参照渡し(下で項目を足す)
const grid = new TssGrid(el, { data, columns, contextMenu: menu });
const cmt = TssComment(grid, {
  rowId: (row) => row.id,                          // 安定キー(ソート/フィルタ/再読込に追従)。既定は行index
  comments: [{ id: 101, dataKey: 'score', text: '要確認' }],   // 初期(保存済み)
  onCommentChange: ({ id, dataKey, text, action }) => save(id, dataKey, text, action),  // 永続化はホストへ
});
menu.push(...cmt.menuItems());                     // 右クリックに「挿入/編集・削除」を追加
  • キーは rowId(row) + 列 data キーで持つ(r/c では持たない=ソート/フィルタ/行削除でズレない)。永続化は持たず onCommentChange で通知するだけ(保存はホストが担当)。
  • opts: rowId / comments / onCommentChange / canComment(r,c) / readOnly(閲覧専用) / labels / className。API: set/get/remove/open/getAll/menuItems/refresh/destroy。
  • グリッドを作り直すホストは、作り直すたびに TssComment(grid, { comments: cmt.getAll(), … }) で貼り直す。見た目は --cmt-* 変数と tg-comment-* クラスで自由に。

同梱プラグイン: 派生列(formula / computed column)

plugins/tss-formula.js=「その行の値だけから計算する列」を columns[c].formula 関数で書けるようにする。金額=数量×単価、税込=金額×1.1 のような派生列を宣言的に。動く例: examples/formula.html。

<script src="plugins/tss-formula.js"></script>
new TssGrid(el, {
  columns: [
    { data: 'quantity', type: 'number' },
    { data: 'price',    type: 'number' },
    { data: 'amount',  type: 'number', formula: (row) => row.quantity * row.price },          // ← 派生列
    { data: 'withTax', type: 'number', formula: (row) => Math.round(row.amount * 1.1) },       // ← 前の formula 結果も使える
  ],
  plugins: [TssFormula],   // これだけ。grid.usePlugin(TssFormula) / plugins:['formula'] でも可
});
  • formula は行スコープの純粋関数 (row, {r,c,grid}) => value(row=その行の {dataキー:値})。文字列ではなく関数で渡す(eval 不要・CSP安全)。
  • formula 列は自動 readOnly / 同じ行が変わると自動再計算(編集・フィル・貼付・Undo/Redo すべて追従)/ 書き込みは履歴に積まない(派生値が独立 Undo 段にならない)/ columns 定義順に評価=後ろの formula は前の結果を使える。
  • 値は内部文字列なので算術は JS が数値強制('120' * '18' = 2160)。+ の連結を避けたいときは Number(row.x) を。
  • 列方向の集計(縦の SUM・合計行)は formula の範囲外=下の TssTotals で。

同梱プラグイン: 列の集計(totals / 縦の SUM・平均…)

plugins/tss-totals.js= 列方向の集計(sum/avg/count/min/max/独自関数)を担う。計算だけで描画はしない=結果を onTotals で受けて好きな場所へ(フッタ・ラベル・別の TssGrid の合計表でも)。formula と併用すると派生列の後に集計が連動。動く例: examples/formula.html(明細=formula/合計=totals)。

<script src="plugins/tss-totals.js"></script>
new TssGrid(el, {
  columns: [ /* …amount 等… */ ],
  plugins: [ TssFormula, TssTotals({
    columns: { quantity: 'sum', amount: 'sum', price: 'avg' },   // dataキー → 集計種別
    skipEmpty: true,                                              // 全列空の行を除外(既定true)
    onTotals: (t) => { footerEl.textContent = '合計 ' + t.amount; },  // 再計算のたびに呼ばれる
  }) ],
});
// 取得は handle 経由でも: grid.getPlugin('totals').getTotals()  // → { quantity:…, amount:… }
  • コアに合計行を埋めない理由=置き場が minSpareRows/ソート/空行処理と衝突して肥大化するため。集計を外出しして「どこに出すかは自由」にした。
  • formula と併用時は plugins: [TssFormula, TssTotals(...)] の順(合計を派生列の結果の後に計算するため)。sum/avg/min/max は数値セルのみ・count は非空セル数。

同梱プラグイン: 条件付き合計(tss-sum / ラベル付き合計行・整列表示)

plugins/tss-sum.js= ラベル付きの"合計行"(小計・消費税10%・消費税8%・合計 など)を、条件(when)+対象(of)を宣言で定義して作る。tss-totals が「列ごと1値」なのに対し、こちらは同じ列を条件別に何本でも集計でき(消費税10%と8%は両方 amount 由来=per-column では衝突する)、ソースと同一列構成の readonly 集計グリッドを生成するので金額が縦にピッタリ揃う(画面でも印刷でも)。請求書・見積などの合計欄に。動く例: examples/invoice.html(インボイス請求書の合計欄)。

<script src="plugins/tss-merge.js"></script>  <!-- ラベルの横結合に利用 -->
<script src="plugins/tss-sum.js"></script>
const sum = TssSum.attach(grid, document.getElementById('sumgrid'), {
  format: v => '¥' + Number(v).toLocaleString('ja-JP'),
  rows: [
    { label: '小計(税抜)',  of: r => (+r[2]||0)*(+r[3]||0) },                                  // 全データ行の合計
    // 条件付き+集計後フック: of で税抜を集めて、after でその合計に 1 回だけ端数処理
    { label: '消費税(10%)', when: r => r[4]==='10%', of: r => (+r[2]||0)*(+r[3]||0), after: s => Math.round(s*0.10) },
    { label: '消費税(8%)',  when: r => r[4]==='8%',  of: r => (+r[2]||0)*(+r[3]||0), after: s => Math.round(s*0.08) },
    { label: '合計(税込)',  total: v => v[0]+v[1]+v[2] },                                      // 既出行から派生
  ],
  onCompute: vals => { /* 上部の総額表示など */ },
});
// sum.refresh() 手動再計算 / sum.detach() フック解除+集計グリッド破棄
  • 行は {label, when?, of, after?}(when 一致行で of(row) を合計=SUMIF)または {label, total}(既出行の値から派生)。row は内部配列(列index)で受ける。
  • after(sum)=集めた合計に対して最後に1回だけ走る(行ごとではない)。適格請求書の消費税は「税率ごとに1回だけ」端数処理するのが要件で、of の中で丸めると明細行ごとになってしまうため(105円×2行@10%: after=端数処理(210×0.10)=¥21 / 行ごと=floor(10.5)×2=¥20 で1円ずれる)。when で税率ごとに行が分かれるので、after はその税率の合計に対して走る(請求書全体で1回ではない)。after の結果は後続の total(vals) にも渡る=合計(税込)が丸め後の税額を見る。
  • ソースの onAfterChange/onStructureChange を自動で包んで再計算=編集・行追加削除・Undo すべてに追従。集計グリッドは表示専用(同一列幅で 金額 列が揃う)。

同梱プラグイン: 残高/累計(running total・縦の連鎖)

plugins/tss-running.js=「前の行の結果 + この行の増減」で縦に積む列。formula(行内)でも totals(全体合計)でも出せない縦方向の連鎖を担う。出納帳/通帳の残高、在庫受払の在庫数、経費・ポイントの累計など。動く例: examples/running.html(現金出納帳=入金・出金→残高)。

<script src="plugins/tss-running.js"></script>
new TssGrid(el, {
  columns: [ /* …deposit, withdraw, balance… */ ],
  plugins: [ TssRunningTotal({
    column: 'balance', initial: 10000,                                       // 残高列(自動readOnly)/前残
    step: (prev, row) => prev + Number(row.deposit||0) - Number(row.withdraw||0),
  }) ],
  // 単純累計なら step の代わりに delta: 'amount'(running += Number(row.amount))
});
  • 編集・行の挿入/削除・setData(+それらの Undo/Redo)で上から全部引き直し。書き込みは履歴に積まない。
  • column(残高列) / initial(前残) / delta か step / skipEmpty(全列空の行は連鎖を進めず空表示、既定true)。
  • 行ドラッグ移動(rowReorder)併用時は順序が変わるので onAfterRowMove で handle.recompute() を呼ぶ。

「行=formula/列=totals/縦=running」の3点が揃う。いずれも setValueRaw+onAfterChange ラップで動くプラグイン。

同梱プラグイン: 氏名→フリガナ 自動入力(furigana・辞書不要)

plugins/tss-furigana.js(辞書不要)= 漢字を入力(IME変換)すると読みが別列に自動で入る(スマホ連絡先の よみがな 自動入力と同じ)。辞書引きではなく、IME変換の composition イベントから「変換前のかな」を拾う方式。TssGrid はアクティブセルに本物の <input> を重ねる設計なので composition が確実に取れる=「IMEに強い」を機能で体現。動く例: examples/furigana.html。

<script src="plugins/tss-furigana.js"></script>
new TssGrid(el, {
  columns: [ { data: 'name', title: '氏名' }, { data: 'kana', title: 'フリガナ' }, /* … */ ],
  plugins: [ TssFurigana({ source: 'name', target: 'kana', katakana: true }) ],
  // 複数ペアは pairs: [{source,target}, …] / ひらがな出力は katakana:false
});
  • source(漢字を入れる列) → target(読みを入れる列)。target は readOnly にしない=手修正できる。
  • 正直な限界: 手入力(IME変換)した時だけ取れる。貼り付け・既存の漢字からは取れない(辞書を持たないため)。「自動入力+手直し」が現実解。

同梱プラグイン: セル内ミニ計算式(calc・=12*3)

plugins/tss-calc.js(eval/Function 不使用)= セルに =12*3+5 と打つと確定時に 41 に。四則 + - * /・括弧・小数対応の自前再帰下降パーサ(CSP安全)。columns[c].parse(入力変換)seam に乗るだけ。動く例: examples/calc.html。

<script src="plugins/tss-calc.js"></script>
// (a) 列の parse に直接(一番素直)
columns: [ { data: 'amount', type: 'number', parse: TssCalc() } ]
// (b) プラグインで対象列にまとめて注入(既存 parse があれば合成)
plugins: [ TssCalc.plugin({ columns: ['quantity', 'price'] }) ]   // columns 省略で number 列すべて
  • = 始まりは強制評価、= 無しの素の式(120*1.1)も計算(bare:false で無効化)。ただの数値・文字は素通り。構文エラー/0除算は入力を残す(validator が弾く)。
  • prefix(既定 '=') / bare(既定 true) / decimals(結果の小数桁)。formula と併用すると「式で入力→派生列が再計算」も連動。

同梱プラグイン: オートコンプリート(autocomplete エディタ)

plugins/tss-autocomplete.js= 自由入力+候補の絞り込み。固定 dropdown の上位版で「だいたい決まってるが新規もある」項目(取引先・商品名)向け。IME変換中の Enter は奪わない(変換確定が最優先)。動く例: examples/autocomplete.html。

<link rel="stylesheet" href="plugins/tss-autocomplete.css">
<script src="plugins/tss-autocomplete.js"></script>
columns: [
  { data: 'client', editor: TssAutocomplete({ source: ['三星商事','三星製作所', …] }) },          // 自由入力可
  { data: 'pay',    editor: TssAutocomplete({ source: ['現金','振込', …], strict: true }) },        // 候補のみ
];
// 動的候補: source: (query, {row,r,c,grid}) => string[]
  • ↑↓で候補移動・Enter確定・Esc取消・クリック選択。source(配列 or 関数) / strict(候補のみ) / minChars / max / match(includes/startsWith/関数) / openOnClick。
  • カスタムエディタ契約(editor.open/close)に乗るだけ。見た目は tss-ac-* クラスと --ac-* 変数で。

同梱プラグイン: コード付きプルダウン+その他自由入力(tss-combo)

plugins/tss-combo.js= 「選択肢から選ぶ。ただし『その他』を選んだら自由入力」を、業務側でその他かどうかを分岐せず (コード, 値) のペアで扱えるようにするエディタ。コードはそのまま(選んだ項目の code/その他は固定 code)、値だけが「選択ラベル or 入力文字列」に変わる。動く例: examples/combo-other.html(tss-combo の各パターン+ autocomplete / 2セル方式の比較も)。

<link rel="stylesheet" href="plugins/tss-combo.css">
<script src="plugins/tss-combo.js"></script>
columns: [
  { data: 'payCode', readOnly: true },                  // コード列(hidden 運用も可=hiddenColumns:[…])
  { data: 'payName', editor: TssCombo({
      options: [ { code:'01', label:'現金' }, { code:'02', label:'振込' } ],
      other:     { code:'99', label:'その他' },          // 選ぶと入力欄に切替(省略で自由入力なし)
      codeField: 'payCode',                              // コードの書き戻し先(data キー or 列 index)
  }) },
];
// 通常項目 → (payCode, payName) = (その code, label)/その他 → ('99', 入力文字列)
  • 値列にエディタを付け、codeField で別のコード列へ code を書き戻す(setValueRaw=派生値・readOnly 列にも書ける)。options は {code,label} か文字列配列(code=label)。既存の「その他」値(選択肢に無い値)は開くと入力欄に復元。
  • undo/redo・貼付で code を値に追従させたい時は TssCombo.codeOf({options, other}) を onAfterChange に噛ませる(任意)。
  • 見た目は tg-combo* クラス、色は --tg-accent を流用。openOnClick / otherPlaceholder / icon / className。
  • 開いたリストの見た目はネイティブ type:'dropdown'(OS 描画の <select>)とはあえて別(自前ポップアップ=角丸・影・アクセント色=「ここは『その他』入力もできる」合図)。セル右の ▾ キャレットはネイティブ dropdown と統一済み。リストを素の dropdown に寄せたい場合は .tg-combo* / --tg-accent を上書きすれば調整できます(※逆にネイティブ <select> のリストは OS 製で CSS 装飾不可)。

同梱プラグイン: セル内スパークライン(sparkline・ミニグラフ)

plugins/tss-sparkline.js(SVG自前描画)= セルの数列をミニ折れ線/棒で描く。columns[c].html:true + format(format が返す SVG がセルに入る)seam に乗るだけ。Chart.js 等は積まない。動く例: examples/sparkline.html(月次売上→動向グラフ・編集でライブ更新)。

<script src="plugins/tss-sparkline.js"></script>
// 値 "12,15,9,20,18"(カンマ/空白区切り)→ ミニ折れ線
{ data:'trend', title:'動向', html:true, readOnly:true, format: TssSparkline({ width:96, height:24, fill:'rgba(13,148,136,.12)' }) }

// formula で月次列から数列を組み立てると、月を編集→グラフがライブ更新
{ data:'trend', html:true, readOnly:true, format: TssSparkline(),
  formula: (row) => ['m4','m5','m6','m7','m8','m9'].map(k=>row[k]).filter(v=>v!=='').join(',') }
  • type('line'/'bar') / width / height / color / strokeWidth / fill(折れ線下の塗り) / min・max(既定は自動)。
  • **fill を指定すると折れ線の下が塗られて「面グラフ(area)」**になります(=推移+量感)。fill 無し=ただの線(line)。別プラグイン不要で line/area を使い分け。

同梱プラグイン: セル内ハイパーリンク(tss-hyperlink・安全URL)

plugins/tss-hyperlink.js= セルの値をクリックで開くリンクに(format ヘルパ)。値は 「表示テキスト|URL」 か URL 単体。columns[c].html:true + format: TssHyperlink() で使います。動く例: examples/hyperlink.html。

<link rel="stylesheet" href="plugins/tss-hyperlink.css">
<script src="plugins/tss-hyperlink.js"></script>
columns: [
  { data:'url', html:true, readOnly:true, format: TssHyperlink() },
];
// 値: 'GitHub|https://github.com' / 'https://example.com' / 'example.com' / 'メール|mailto:[email protected]'
  • ★安全URL: http/https/mailto/tel のみリンク化。javascript:・data: 等は素のテキストに落とす(XSS 無効化)。スキーム無しは https:// 補完、表示テキスト・href は HTML エスケープ、target=_blank に rel="noopener noreferrer" を付与。
  • separator(既定 '|') / target(既定 '_blank') / className(既定 'tss-link')。TssHyperlink.safeUrl(u) で URL 安全化だけ単体利用も可(許可外は null)。
  • 数値しか SVG に入れない=安全。formula/html-cells と同じ仕組みで、有料グリッドの "セル内グラフ" を無料で。

同梱プラグイン: セル内 横棒グラフ(bar・データバー)

plugins/tss-bar.js= セルの数値を横棒で描く(認知率・進捗%・スコア)。html:true+format seam。type:'number' 併用でバーのまま編集(値を打つと伸びる)。動く例: examples/bar.html。

<script src="plugins/tss-bar.js"></script>
// 表示専用
{ data:'rate', title:'認知率', html:true, readOnly:true, format: TssBar({ max:100, suffix:'%', decimals:1 }) }
// バーのまま編集(値を打つと伸びる)+ 閾値で色分け
{ data:'rate', type:'number', html:true, format: TssBar({ max:100, suffix:'%', color: v => v < 40 ? '#dc2626' : '#0d9488' }) }
  • min/max(既定0/100) / color(文字列 or (value)=>色=閾値色分け) / track / height / radius / showValue / decimals / suffix / labelWidth。

同梱プラグイン: ヒートマップ / 背景データバー(cellStyle)

plugins/tss-heatmap.js= コアの cellStyle フックに乗せる連続色(ヒートマップ)と背景データバー。tss-bar.js(前景の棒+ラベル)と違い、セル背景を塗る=数値はそのまま見せて編集できる。動く例: examples/heatmap.html。

<script src="plugins/tss-heatmap.js"></script>
// ヒートマップ: 低→高で背景連続色。文字色は自動コントラスト。
{ data:'sales', type:'number', cellStyle: TssHeatmap({ min:100000, max:400000, colors:['#f8b4b4','#fff','#93c5fd'] }) }
// 背景データバー: 数値はそのまま・背景に棒(編集可)
{ data:'rate', type:'number', cellStyle: TssDataBar({ min:0, max:100, color:'rgba(13,148,136,.22)' }) }
  • TssHeatmap: min/max(明示。セル単位呼び出しで列範囲を知らないため)/ colors(2色以上のストップ) or low/high / text(自動文字色, 既定on)。
  • TssDataBar: min/max / color / align('left'/'right')。編集で即更新。

「セル内グラフ」3点: 前景の折れ線/棒=html:true+format(tss-sparkline / tss-bar)、背景の連続色/バー=cellStyle(tss-heatmap)。どれもノービルド。

グラフ連携(Chart.js などと)— 編集でライブ更新

セル内グラフより大きく見せたいときは、外部チャートライブラリ(MIT)と連携。データの持ち主はグリッド、グラフはその"ビュー"です。連携は onAfterChange で grid.getRows() → chart.data → chart.update() するだけ。コアには積まず CDN+SRI のサンプルとして提供(excel-export と同じ位置づけ)。動く例: examples/chart-chartjs.html(Chart.js・MIT・編集で折れ線/棒がリアルタイム更新)。

const chart = new Chart(ctx, { type:'line', data: toChartData(grid) });
new TssGrid(el, { columns, data, onAfterChange: () => { chart.data = toChartData(grid); chart.update(); } });
  • おすすめ(いずれも <script>/CDN で no-build): Chart.js(MIT・定番・バランス◎)/ ApexCharts(MIT・モダンで映え)/ Apache ECharts(Apache-2.0・多機能)/ uPlot(MIT・超軽量)。Highcharts/AG Charts は商用有料なので MIT 方針なら避ける。

同梱プラグイン: 2択トグル(toggle・有効|無効)

plugins/tss-toggle.js+.css= ラジオの2択を並んだセグメント(有効|無効)で表示、クリックで切替。チェックボックスより2ラベルが見える。値は dropdown 同様 value を保存。動く例: examples/toggle.html。

<link rel="stylesheet" href="plugins/tss-toggle.css">
<script src="plugins/tss-toggle.js"></script>
plugins: [ TssToggle.plugin({ columns: {
  status: ['有効', '無効'],                                      // 文字列2つ(value=label)
  pub:    [{ value:'1', label:'公開' }, { value:'0', label:'非公開' }],  // value/label 分離
} }) ]
  • クリックでその側に確定(setValue=検証・Undo を通る)。アクティブセルで Space で反対側へトグル。2値以外の打ち込みは自動 validator で拒否。
  • 単純な ON/OFF 1個なら組込 type:'checkbox'、3択以上は type:'dropdown'。見た目は --tg-toggle-accent 等で。

同梱プラグイン: セル結合(merge・横/縦/矩形)

plugins/tss-merge.js= データセルの矩形結合(colspan×rowspan)。左上のアンカーが値・編集・選択の対象で、覆われた従属セルは描画スキップ+読み取り専用(貼り付け・フィルは弾く)。動く例: examples/merge.html。(見出しの結合は nestedHeaders を参照)

<script src="plugins/tss-merge.js"></script>
plugins: [ TssMerge.plugin({ merges: [
  { r:0, c:0, colspan:1, rowspan:2 },   // 縦結合(2行)
  { r:0, c:2, colspan:2 },              // 横結合(rowspan 省略=1)
] }) ]

| API | 説明 | |---|---| | grid.setMerge(r, c, colspan, rowspan?) | (r,c) を起点に結合。rowspan 省略=横結合。従属セルの値はクリア(Excel流)。成功で true、範囲外/1×1/既存結合と重なれば false | | grid.mergeSelection() | 選択中の矩形を結合 | | grid.removeMerge(r, c) | (r,c) を含む結合を解除 | | grid.getMerges() | [{r,c,colspan,rowspan}, …] | | grid.destroyMerged() | 全結合を解除 |

  • Undo 対応: setMerge/removeMerge/mergeSelection/destroyMerged は履歴に積み、結合時にクリアした値も復元(初期 merges は設定扱いで非対象)。
  • 行/列の挿入削除で結合座標が縦横対称に追従(拡張・縮小・解除、Undo 対応)。
  • 割り切り: ソートは結合中ロック。行移動は横結合のみ可(行と一緒に移動)、縦結合がある間はロック(行順を崩すと縦結合が分断=auto-split を避ける)。
  • 既知の限界: frozen 境界をまたぐ結合は想定外。結合範囲のコピーは従属位置が空セル。

同梱プラグイン: 同期スクロール(syncscroll・左右2表を並べて比較)

plugins/tss-syncscroll.js= 並べた複数グリッドのスクロールを連動(Excel「並べて比較+同時にスクロール」相当)。A系/B系で別々に絞り込んだ表を突き合わせながら編集する用途に。動く例: examples/sync-scroll.html。

<script src="plugins/tss-syncscroll.js"></script>
const link = TssSyncScroll.link([gridA, gridB]);                 // 既定: 相対・縦横ピクセル
const link = TssSyncScroll.link([gA, gB], { by: 'row' });        // 行で揃える(行高が違っても対応行が並ぶ)
const link = TssSyncScroll.link([gA, gB], { relative: false });  // 絶対(スナップして一致)
link.unlink();                                                   // 連動解除
  • relative: true(既定・Excel流)= ON した時点の位置差を保ったまま移動量(delta)だけ連動。連動OFF→片方スクロール→ON でも“その場から”続けて同期できる。false で絶対スナップ。
  • by: 'pixel'(既定・スクロール量) / 'row'(コアの実測行高キャッシュ _rowTops で行を揃える=左右で行高が違っても対応行が並ぶ)。
  • axis: 'both'(既定) / 'vertical' / 'horizontal'。link() は { unlink, rebase, grids } を返す(rebase()=相対モードで“今の差”を基準化)。

実例: 在庫管理ミニアプリ(機能てんこ盛り)

TssGrid 1本(+カレンダープラグイン)で組んだ業務画面のショーケース → 動く例: examples/inventory.html。項目を差し替えれば勤怠・発注・工程などにも転用できます。

入っている機能: ドロップダウン / 数値書式 / 計算列(金額=数量×単価, 読み取り専用)/ チェックボックス+ヘッダ全選択 / 休日カレンダー / 入力検証 / ヘッダクリックで並べ替え / 右クリックで行挿入削除・全チェック / 末尾に常に空行 / Excel相互コピペ・Undo / getRows() で保存 / CSV出力(BOM付)。

計算列は、読み取り専用セルへ setValue(r, c, val, /*force*/ true) で書き込みます(プログラムからの確定的な書き込みは readOnly を貫通=ユーザー編集だけ保護)。

onAfterChange: (changes) => {
  for (const ch of changes) if (ch.c === 数量 || ch.c === 単価) {
    const amt = String((+grid.getValue(ch.r, 数量) || 0) * (+grid.getValue(ch.r, 単価) || 0));
    grid.setValueRaw(ch.r, 金額, amt);   // ★ 計算列は setValueRaw(履歴に積まない派生値・readOnly貫通)
  }
}

計算列は setValueRaw で書くのが正解です。setValueRaw(r,c,val) は履歴に積まず・検証も通さず値を直接セット(readOnly も貫通、表示書式は描画時に適用)。これにより計算結果は独立した undo ステップにならず、undo/redo では入力セルが履歴復元→onAfterChange で自動的に再計算されて追従します(入力1つ=undo1回・計算列は勝手に付いてくる)。 ※ 逆に setValue(...,true)(履歴あり)で計算列を書くと、計算結果が別の undo ステップになり、undo/redo 中の再計算が redo スタックを壊します。派生値は必ず setValueRaw。

実例: 方眼ガント(予定/実績・遅延管理)

専用ライブラリを積まず、コアだけで組んだ方眼ガント+遅延管理ツール → 動く例: examples/gantt.html。依存ゼロ。

入っている表現: 予定/実績の2段バー(実績が予定より延びると遅延を赤で)/ 状態列(完了・進行中・遅延・未着手)/ 期間&「今日」線 / 進捗データバー(cellStyle だけで描画)/ 曜日+土日色(nestedHeaders のリーフに改行を入れ buildTable ラップで色分け)。日付は手入力可(format+parse+invalidMode:'keep' で不正日を赤検証)。

「ガントの“升目”はグリッドそのもの」という割り切りで、専用コンポーネントなしにスケジュール表をそのまま画面化できます。バーをドラッグして日付を動かす版は将来候補(コアは無改変のまま外側で組める)。

テーマ / 見た目のカスタマイズ

主要な色・サイズは CSS 変数にしてあり、業務側で数個上書きするだけで統一感を出せます(フルのテーマエンジンは持たない軽量方針)。

.tssgrid { --tg-accent: #c0392b; --tg-header-bg: #f3eaea; --tg-font-size: 14px; }  /* 全グリッド */
.myapp .tssgrid { --tg-edit-accent: #7b61ff; }                                      /* 特定業務だけ */

主な変数: --tg-accent(選択枠/フィル) / --tg-edit-accent(編集枠) / --tg-border / --tg-wrap-border / --tg-header-bg(ヘッダ背景の既定) / --tg-colheader-bg(列ヘッダ背景) / --tg-rowhead-bg(行番号列背景) / --tg-header-fg(列ヘッダ文字)・--tg-rowhead-fg(行番号文字) / --tg-cell-bg / --tg-sel-bg(選択背景) / --tg-selhdr-bg(選択中ヘッダ) / --tg-readonly-bg・--tg-readonly-fg / --tg-placeholder-fg / --tg-invalid-fg・--tg-invalid-bg / --tg-box-color・--tg-box-width(セル囲み) / --tg-font-size / --tg-radius。さらに細かくは従来どおり .tssgrid .tg-xxx { … } で上書きできます(全 CSS は .tssgrid 配下に scope 済み)。

  • ヘッダ色: --tg-header-bg で列ヘッダ+行番号列をまとめて、または --tg-colheader-bg/--tg-rowhead-bg で別々に(未指定なら --tg-header-bg を継ぐので後方互換)。選択中の列/行ヘッダは --tg-selhdr-bg が出るので、濃色テーマではこれも合わせると統一感が出ます。

罫線まわり: 色はセル全体なら --tg-border、セル単位なら cellClass で border-right/bottom-color を指定(各セルは右・下の罫線を持つ設計)。任意の1セルを四角く囲むなら border ではなく同梱の .tg-box クラスを cellClass で付与(box-shadow: inset ベースなので A1・1行目・1列目・端セルでも全4辺を囲め、レイアウトも動きません)。色/太さは --tg-box-color・--tg-box-width、用途別は .tssgrid td.tg-box-warn{ --tg-box-color:#e67e22 } のように。

cellClass: (r, c, value, row) => isAlert(r, c) ? 'tg-box tg-box-warn' : ''   // 該当セルを橙枠で強調

列の表示/非表示

列をプログラムから隠せます(メニュー項目は付きません。hiddenColumns で初期指定も可)。

grid.hideColumn(1);          // 列1を隠す
grid.showColumn(1);          // 再表示
grid.toggleColumn(1);        // 切替
grid.isColumnHidden(1);      // → true/false
grid.getHiddenColumns();     // → [1, 3]
new TssGrid(el, { hiddenColumns: [1] });   // 初期で隠す
  • 隠し列は畳まれ(幅0)、カーソル移動は隣の可視列へスキップ、幅計算・固定列オフセットからも除外されます。
  • コピーは隠し列を除外し、貼り付けは可視列へ流し込みます(見たままをコピペ)。
  • データは消えません(表示だけ。getData()/getRows() には隠し列の値も入ります)。
  • 最低1列は残ります(全列は隠せません)。列の挿入・削除をしても隠し状態のインデックスは自動で追従します。

API

new TssGrid(container, options)

| option | 型 | 既定 | 説明 | |---|---|---|---| | headers | string[] | ['A','B','C'] | 列見出し | | data | string[][] | — | 初期データ(行 × 列) | | history | HistoryManager | 自前生成 | 渡すと Undo を共有 | | name | string | 'grid' | 履歴ラベル / 識別 | | showDump | boolean | false | データ確認用の表示 | | enterMoves | 'down'\|'up'\|'right'\|'left'\|'none' | 'down' | Enter 確定後の移動方向 | | tabMoves | 'down'\|'up'\|'right'\|'left'\|'none' | 'right' | Tab 確定後の移動方向 | | initialCell | [r,c] or {r,c} | [0,0] | 初期カーソル位置。c は index か data キー(例 {r:0, c:'start_hours'}) | | nextCell | ({r,c,key,shift})=>{r,c}\|null | — | 入力フロー制御: Enter/Tab の移動先を上書き(null で既定方向)。入力セルだけを論理順で巡回でき、readonly 列を飛ばせる(業務フォーム向け)| | columns | ColumnDef[] | [] | 列ごとのセルタイプ / バリデータ(下記) | | invalidMode | 'revert'\|'keep' | 'revert' | 検証 NG 時。revert=元の値に戻す / keep=入力値を残してセルを赤く警告 | | invalidTitle | boolean | true | keep 時、赤セルに validator メッセージを title=(ホバーで理由)。false でOFF。全件は getInvalidCells() | | messages | {code:…}\|fn | — | エラーの共通メッセージカタログ。validator が {code,params,level} を返すと本文を解決({label}/{value}/params 差込)。本文を外部JSON/CSV/XMLで一元管理 | | pasteOverflow | 'clip'\|'error' | 'clip' | 貼り付けがグリッド外に超過した時。clip=はみ出し分を切り捨て / error=貼り付け中止 | | pasteAutoGrow | boolean | false | 貼付元の行数がグリッド行数を超える時、不足分を insertRows で先に一括生成してから貼る(Excel から貼れば貼った行数だけ行が増える)。行生成+値貼付は1回の undo で両方戻る。maxRows 上限内・超過分は pasteOverflow に従う。既定 false=従来どおりはみ出しは切り捨て | | colWidths / rowHeights | number[] | {[key]:number} | [] | 列幅 / 行高の初期値(px)。未指定の要素は既定値。colWidths は配列(位置指定)でも、{ dataキー: px } のオブジェクト(列ID指定)でも可=後者は列を入れ替えても幅が追従する。列定義側に columns[c].width で同居指定も可(並べ替え・コピペで幅が一緒に動く=最も壊れにくい)。優先度: 実行時リサイズ/明示 colWidths > columns[c].width > 既定 | | defaultColWidth / defaultRowHeight | number | 120 / 28 | 既定の列幅 / 行高(px) | | minColWidth / minRowHeight | number | 30 / 18 | リサイズの下限(px) | | resizeMode | 'preview'\|'live' | 'preview' | リサイズの見せ方。preview=Excel風に線で予告→離して確定 / live=即時反映 | | frozenCols | number | 0 | 左から固定する列数(横スクロールしても残る)。freezeCols(n) で実行時変更も可 | | frozenRows | number | 0 | 上から固定する行数(縦スクロールしても残る)。freezeRows(n) で実行時変更も可。frozenCols 併用で四隅固定 → 動く例: examples/frozen-rows.html | | nestedHeaders | array[] | — | ヘッダ結合(複数段見出し)。各段は配列で要素は文字列 or {label,colspan}。最下段がリーフ=実列(並べ替え・全選択チェック等は機能)。上段グループのクリックで範囲列を選択 → 動く例: examples/nested-headers.html | | width / height | number\|string | — | 枠(wrap)の幅/高さ。指定するとテーブル自身に横/縦スクロールバーが出る(未指定だと枠が内容幅まで広がりページ側がスクロール)。固定行/列・縦横スクロールを使うなら指定推奨 | | resizeCols / resizeRows | boolean | true | 列幅/行高のリサイズ可否(全体)。false でグリップを出さない | | cursor | boolean | true | 選択カーソル(青の選択枠/範囲/セルハイライト/フィルハンドル)を表示するか。false で表示専用グリッド向けに非表示+セルカーソルも既定に(内部の選択は保持=コピーは可)。合計表・サマリ等に | | columns[c].width | number | — | 列ごとの初期幅(px)。列定義に同居するので並べ替え・コピペで幅が一緒に動く(colWidths より壊れにくい)。実行時リサイズ・明示 colWidths があればそちらが優先 | | columns[c].resizable | boolean | true | 列ごとの幅ロック。false でその列だけリサイズ禁止 | | stretchH | 'none'\|'last'\|'all' | 'none' | 列を枠幅まで伸ばす。'last'=余りを最終列へ / 'all'=全列で配分(width 指定時に効く) | | rowHeaders / colHeaders | boolean | true | 行番号列 / ヘッダ行の表示。false で隠す | | hiddenColumns | number[] | [] | 初期で隠す列インデックス。実行時は hideColumn/showColumn/toggleColumn(→ 列の表示/非表示) | | wordWrap | boolean | false | セル内で折り返し(行高は内容に合わせ伸びる)。columns[c].wordWrap で列単位も可 | | placeholder | string | — | 空セルに薄いヒント文字(getData には入らない)。columns[c].placeholder で列単位も可 | | className | string | — | コンテナ(.tssgrid)に付ける任意クラス(テーマ付け) | | fillHandle | boolean \| {direction,autoInsertRow} | true | falseでフィル完全OFF(ハンドルを出さない=ドラッグでの消去/上書きなし。フィルの起点はハンドルのみなので fill 自体が無効に)。{direction:'vertical'\|'horizontal'\|'both', autoInsertRow:true}で詳細指定(下へドラッグで行追加)。設定/表示専用グリッドで誤フィルを防ぐのに有効 | | onBeforeAutofill / onAfterAutofill | (src,target)=>… | — | フィル前後。before は false で取消。src/target = {r0,c0,r1,c1} | | autoColumnSize | boolean \| number | false | 初期化時に列幅を内容に合わせて自動調整(数値=上限px)。autoSizeColumn/autoSizeAllColumns API・列境界ダブルクリックでも | | columns[c].align / valign | string | — | 列既定の整列(→ 整列) | | columns[c].editor | object \| ()=>object | — | カスタム編集UI(→ カスタムエディタ)。editor.inline:true で打鍵の1文字目・IME 直打ちを殺さないサジェスト型に(→ editor.inline) | | copyPaste | boolean | true | false でコピー/カット/ペーストを無効化 | | minSpareRows | number | 0 | 末尾に常に確保する空行数(Excel風に下へ打ち足せる) | | autoWrapRow / autoWrapCol | boolean | false | Tab で行末→次行頭 / Enter で列末→次列頭に折り返し | | minRows / maxRows | number | — | 行数の下限/上限(min は初期パディング、削除の床。max は天井=挿入・minSpareRows の空行・フィルハンドルの自動追加すべてを打ち止める) | | minCols / maxCols | number | — | 列数の下限/上限(max は挿入の天井) | | readOnly | boolean | false | グリッド全体を読み取り専用に(ビューア用途) | | columns[c].readOnly | boolean \| (value,{r,c})=>boolean | — | 列単位の読み取り専用。関数を渡すとセル単位で判定(→ 読み取り専用) | | onChange | (grid)=>void | — | データ変更時に呼ばれる(後方互換) | | onInvalid | (rejections, source)=>void | — | 列ルールで弾かれた時。rejections = [{ r, c, value, message }] | | onReadOnly | (blocked, source)=>void | — | 読み取り専用セルへの変更を弾いた時。blocked = [{ r, c, value }] | | contextMenu | boolean \| Array \| {items} | true | 右クリックメニュー。true=既定項目 / false=無効 / 配列=項目を構成(組込キー+カスタム項目)(→ 右クリックメニュー) | | allowInsertRows / allowDeleteRows | boolean | true | 行の挿入 / 削除の可否(メニュー項目の表示も連動) | | allowInsertCols / allowDeleteCols | boolean | true | 列の挿入 / 削除の可否 | | onStructureChange | (info)=>void | — | 行/列の挿入・削除後。info = { type, ... , undo? } | | onBeforeStructureChange | (info)=>false\|void | — | 行/列の挿入・削除の前。false で中止 | | onAfterPaste | (data,{range})=>void | — | 貼り付け確定後 | | onBeforeCopy / onBeforeCut | (data,{range})=>false\|string[][]\|void | — | コピー/カットの前。false で取消・配列で差替え | | onAfterCopy | (data,{range,cut})=>void | — | クリップボード書込後(cut でも発火) | | onBeforeSelectionChange | ({range,active,extent})=>false\|void | — | 選択変更の前。false で中止 | | onBeforeKeyDown | (e)=>false\|void | — | キーをグリッドより先に。false / preventDefault() で既定を止める | | onHeaderClick | (c, e)=>false\|void | — | 列ヘッダのクリック。false で既定(列選択)を抑止(ソートUI等を外部で組む用) | | onHeaderRender | (th, c)=>void | — | 各列ヘッダ th の生成後に呼ぶ(再描画毎に自動で再実行)。ソートアイコン等の要素差し込み用 → 動く例: examples/multisort.html | | disjointSelect | boolean | true | Ctrl+ヘッダで行/列を飛び飛びに複数選択(false で Ctrl+クリックは単一選択に戻る)。getSelectedColumns()/getSelectedRows()・飛び飛びコピー/カットと連動 | | shortcuts | Array | [] | カスタムショートカット(→ ショートカット / プラグイン) | | dateShortcuts | boolean | true | 組込 Ctrl+;=今日 / Ctrl+:=現在時刻(false で無効) | | fillShortcuts | boolean | true | 組込 Ctrl+D=上をコピー / Ctrl+R=左をコピー(false で無効) | | todayFormat | string \| (d:Date)=>string | 'YYYY-MM-DD' | Ctrl+; がテキスト等の列に入れる日付形式(トークン YYYY/MM/DD)。date 列は列準拠 | | nowFormat | string \| (d:Date)=>string | 'HH:MM' | Ctrl+: がテキスト等の列に入れる時刻形式(トークン HH/mm)。time 列は列準拠 | | plugins | Array | [] | プラグイン(関数 or 登録名)。init(grid) で初期化 | | onPasteOverflow | (info)=>void | — | pasteOverflow:'error' で超過時。info = { anchor:{r,c}, height, width, rows, cols, overRows, overCols } |

enterMoves / tabMoves は Shift 押下で逆方向に移動します(例: 既定で Shift+Enter は上、Shift+Tab は左)。プロパティとして実行時に書き換えても次のキー入力から反映されます(例: grid.enterMoves = 'right')。

入力フロー制御(nextCell): Enter/Tab の移動先を上書きして、入力セルだけを論理順で巡回できます(readonly の計算列を飛ばす/行末で次行の先頭入力へ)。"Excel ではなくシステム" だからできる、フォームに近い入力体験(→ 実例: examples/attendance.html):

const FLOW = [3, 4, 5, 13];                    // 出勤→退勤→所定外休→備考(入力列だけ)
new TssGrid(el, {
  initialCell: { r: 0, c: 3 },                 // 初期カーソル=1日目の出勤
  nextCell: ({ r, c, shift }) => {
    const i = FLOW.indexOf(c);
    if (i < 0) return null;                     // 入力列以外は既定移動
    return i < FLOW.length - 1 ? { r, c: FLOW[i + 1] } : { r: r + 1, c: FLOW[0] };  // 備考→次行の出勤
  },
});

メソッド: getData() / getRows() / getRow(r) / getColumn(cOrKey) / getColumns() / setData(rows) / appendRows(rows,opts?)(背景で"育てる"追記=data末尾に足し可視窓だけ再描画。buildTable全再描画せずスクロール位置・選択・編集中エディタを保ち履歴にも積まない。「先頭ページを描く→残りをidleでappendRows」でfirst-paintを早める土台。既存経路は不変のopt-in) / setRowCount(total) + fillRows(start,rows)(遅延ロード=on-demand=virtual前提。総件数を確保し未取得ぶんは「読込中」行、onViewportChange(start,end) で要る範囲を受け fillRows で実データを流し込む。sort/filterは遅延中ローカル計算せず onSortFilterChange でサーバ委譲、未取得セルは編集不可、export(getData/toCSV)はロード済みのみ=AG Grid の Infinite Row Model 相当を push で) / getValue(r,cOrKey) / setValue(r,cOrKey,val,force?) / setValueRaw(r,cOrKey,val)(履歴に積まない派生値書き込み・計算列用) / hideColumn(c) / showColumn(c) / toggleColumn(c) / isColumnHidden(c) / getHiddenColumns() / setAlignment(align,range?) / getAlignment(r,c) / setCellReadOnly(flag,range?) / autoSizeColumn(c) / autoSizeAllColumns() / sortBy(cOrKey, dir) / sortRows(cmp) / addShortcut(s) / removeShortcut(name) / usePlugin(fn) / getPlugin(name) / destroy() / setActive(r,c) / scrollToRow(r, align?)(指定行までスクロール=HTML の # アンカー相当。align は 'start'〔既定=先頭・sticky ヘッダ直下〕/'center'/'end'/'nearest'〔可視ならそのまま〕。仮想・非仮想どちらでも動き、範囲外 r は自動クランプ。選択は変えないので目印は rowClass 等を併用。例 examples/scroll-to-row.html) / selectRow(r) / selectCol(c) / selectAll() / setColWidth(c,px) / setRowHeight(r,px) / freezeCols(n) / freezeRows(n) / moveRow(from,to) / moveRows(r0,r1,to) / clearSort() / toCSV(opts) / downloadCSV(name,opts) / insertRow(ri,'above'|'below') / insertRows(ri,count?,'above'|'below')(count 行を1回の再描画・1 undo で一括挿入=大量挿入が O(N)) / deleteRows(r0,r1?) / insertCol(ci,'left'|'right') / deleteCols(c0,c1?) / redraw()(列定義変更後の再描画, データ・選択は保持) / renderCell(r, cOrKey)(セル1つだけ表示を描き直す=テキスト/HTML・条件付き書式・無効表示を最新データで。全体 redraw() より軽い。format が外部状態に依存する/セル内ボタンの状態を更新した等に。窓外/非表示は no-op)ほか。

イベント

入力・貼付・フィル・Delete の すべての値変更が同じ changes 形式で同じフックを通るので、検証や整形を 一度書けば全経路に効きます。

new TssGrid(el, {
  headers, data,

  // 確定の“直前”。検証・自動整形のフック。changes = [{ r, c, oldValue, newValue, source }]
  onBeforeChange(changes, source) {
    // ・return false        → 変更を全部取り消す
    // ・change.newValue を書換 → 整形して確定(例: 全角→半角、trim、大文字化…)
    // ・1セルだけ拒否したい   → その change.newValue を oldValue に戻す
    changes.forEach(c => { c.newValue = c.newValue.trim(); });
    if (changes.some(c => c.c === 0 && c.newValue === '')) return false; // 例: A列必須
  },

  // 確定の“直後”。保存・再計算など。source: 'edit'|'paste'|'fill'|'delete'|'undo'|'redo'
  onAfterChange(changes, source) { /* save(grid.getData()) など */ },

  // 編集の開始 / 終了(IME確定後に発火。Esc キャンセルは canceled:true)
  onEditStart({ r, c, value }) {},
  onEditEnd({ r, c, oldValue, newValue, canceled }) {},

  // 選択範囲の変更。before は false で“選択させない”ことも可(特定セルへの移動を禁止 等)
  onBeforeSelectionChange({ range, active, extent }) { /* return false で選択中止 */ },
  onSelectionChange({ range, active, extent }) {}, // range = { r0, c0, r1, c1 }

  // 貼り付けの前後。before は 2次元文字列配列を加工/取消、after は確定後の通知
  onBeforePaste(data) { /* return false で取消 / 加工した string[][] を返すと置換 */ },
  onAfterPaste(data, { range }) { /* 貼り付け確定後。range = 落ちた範囲 */ },

  // コピー / カットの前後。before は false で取消・配列で差替え可
  onBeforeCopy(data, { range }) {},
  onBeforeCut(data, { range }) {},
  onAfterCopy(data, { range, cut }) {},

  // 行/列の挿入・削除の前後。before は false で中止できる
  onBeforeStructureChange(info) { /* info.type = 'insertRow'|'insertRows'|'deleteRows'|'insertCol'|'deleteCols'。return false で中止 */ },
  onStructureChange(info) {},   // 後。undo 時は info.undo = true

  // 遅延ロード(push・on-demand)。setRowCount/fillRows と組で使う
  onViewportChange(start, end) {},   // 描画窓が変わった=「今この範囲が要る」。ここで fillRows(start, 取得した行) する
  onSortFilterChange(state) {},      // 遅延中の sort/filter 要求。ローカル計算せずサーバで並替/絞込→setRowCount+fillRows し直す(state.type='sort'|'filter')
});

onBeforeChange で取り消した変更は 履歴(Undo)にも積まれません。undo/redo では onBeforeChange は呼ばれず、onAfterChange のみ source:'undo'|'redo' で発火します。onBefore* 系(Paste/Copy/Cut/StructureChange/SelectionChange)は false を返すとその操作を中止します(履歴にも積まれません)。挿入/削除・コピー/カットの onBefore* は redo では再発火しません(確定済み操作の再実行のため)。

セルタイプ / バリデータ

columns で列ごとに型と検証を指定できます。検証は onBeforeChange の 手前 に効くので、編集・貼付・フィルのどの経路でも同じルールが適用されます。

new TssGrid(el, {
  headers: ['担当', '状態', '完了', '期限', '数量'],
  columns: [
    {},                                            // text(既定)
    { type: 'dropdown', options: ['未着手','進行中','完了'] },
    { type: 'checkbox' },                          // 既定 ON='1' / OFF=''
    { type: 'date' },                              // ISO(YYYY-MM-DD)。不正値は拒否
    { validator: v => v === '' || /^\d+$/.test(v) ? true : '数値で入力' },
  ],
  onInvalid(rej) { alert(rej.map(x => x.message).filter(Boolean).join('\n')); },
});

| type | 編集 UI | 保存値 / 検証 | |---|---|---| | 'text'(既定) | テキスト入力 | そのまま | | 'dropdown' | <select>(options 必須)。セルに ▾ を表示、1クリックで一覧が開き、選ぶと即確定 | 選択肢 or 空のみ許可。範囲外は拒否。クリア用の空オプションは自動で先頭に付くので options に '' を入れない(入れると空が二重)。allowEmpty:false で空オプションを出さず空値も弾く=ラジオ的な必須選択。options は string[] または {value,label}[]=保存値(value=内部コード)と表示(label)を分離(getData/getRows は value で返る・検証も value で照合) | | 'checkbox' | Space / クリックでトグル | checked(既定 '1') / unchecked(既定 '')。貼付値も正規化 | | 'date' | <input type="date"> | YYYY-MM-DD のみ。実在しない日付(例 2026-02-30)は拒否。全角→半角を正規化(IME ON のまま 2026/01/05 と打っても受理。区切りは - / . + 8桁 20260105 も可) | | 'time' | <input type="time"> / テキスト | HH:MM[:SS](24時間制)で保存。hour12:true で AM/PM 表示・入力。全角→半角を正規化(IME ON のまま 9:30 と打っても受理) | | 'number' | テキスト(編集時は素の数値) | 数値のみ。decimals/thousands/prefix/suffix で表示書式(→ CellFormat)。全角→半角を既定で正規化(123→123、zenkaku:falseで無効化)。右寄せ |

  • validator(value, { r, c }): true/undefined で許可、false で拒否、文字列を返すとエラーメッセージ付きで拒否。型と併用可(型 → validator の順で評価)。
  • 検証 NG 時の挙動は invalidMode で選べます:
    • 'revert'(既定): 元の値に巻き戻す。
    • 'keep': 入力値を残してセルを赤く警告(td.tg-invalid)。値を直せば赤は自動で消えます。フォームのように「後でまとめて直す」運用向き。getData() には不正値も入るので、保存前に再検証してください。
    • keep 時は**赤セルにホバーで理由(title=validator のメッセージ)**が出ます。invalidTitle:false でOFF。
  • どちらのモードでも確定時に onInvalid(rejections, source) が呼ばれます(rejections=[{r,c,value,message}])。
  • 全エラーをまとめて取得: grid.getInvalidCells() → [{r,c,key,value,code,level,message}](今の値を全セル検証)。保存前チェックやエラー一覧パネルの表示に → 動く例: examples/validation.html(赤+ホバー+一覧パネルがライブ更新)。

エラーを コード / レベル / 共通メッセージ で管理(messages カタログ)

業務システム向けに、エラー本文を外部の共通メッセージ(設定系)で一元管理できます(トーン統一・差し替え・多言語)。validator はコードを返すだけにし、本文は messages カタログから解決します。

new TssGrid(el, {
  messages: {                                              // システム共通メッセージ(外部JSON/CSV/XML から)
    E_RANGE: { level: 'error', text: '{label}は {min}〜{max} の範囲で入力してください' },
    W_LONG:  { level: 'warn',  text: '{label}がやや長めです({value})' },
  },
  columns: [{ data:'age', title:'年齢',
    validator: v => v==='' || (+v>=0 && +v<=120) || { code:'E_RANG