@threestarsoftware/tssgrid
v0.1.31
Published
軽量・ノービルド・日本語IME堅牢な編集グリッド(vanilla JS / 依存なし / MIT)。Excel ライクな入力画面を <script> タグ1本で。
Maintainers
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/tssgridimport { 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)。セルに直接打ち始める操作は失われます。
- 矢印/PageUp・Down でカレンダー内を移動する旧型が要るなら
- 見た目は 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色以上のストップ) orlow/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