Jspreadsheet 使い方メモ１（基本）

# はじめに
これは、[Jspreadsheet Advent Calendar 2024](https://qiita.com/advent-calendar/2024/jspreadsheet)の8日目の記事となります。

「[Handsontable 使い方メモ１（基本）](https://qiita.com/opengl-8080/items/ac19deec388c357cd498)」と比較しながら書いていきます。

# Hello World
## 導入と設定
https://qiita.com/yaju/items/97e7093ffafad640f6c8

## 実装

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Jspreadsheet</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jspreadsheet-ce/dist/jspreadsheet.min.css" media="screen">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jsuites/dist/jsuites.min.css" type="text/css" />
</head>
<body>
  <div id="grid"></div>
  <script src="https://cdn.jsdelivr.net/npm/jspreadsheet-ce/dist/index.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/jsuites/dist/jsuites.min.js"></script>
  <script type="text/javascript">
    // 以降の記載をここを埋める -->
  </script>
</script>
</body>
</html>
```

```js
var data = [
  [ '佐藤', 28 ],
  [ '鈴木', 19 ],
  [ '田中', 25 ]
];

var grid = document.getElementById('grid');

new jspreadsheet(grid, {data: data});
```

## 実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/f044e893-0942-cd4b-97c6-6c656bd5bdb8.png)

## 説明
- 以下の4つのファイルを読み込む
    - `jspreadsheet-ce/dist/index.min.js`
    - `jsuites/dist/jsuites.min.js`
    - `jspreadsheet-ce/dist/jspreadsheet.min.css`
    - `jsuites/dist/jsuites.min.css`
- グリッドを表示する場所として `<div>` タグを用意して、その Element オブジェクトを取得する
- `jspreadsheet` を `new` してテーブルを作成する
    - コンストラクタ引数の第一引数に先ほど取得した Element オブジェクトを渡す
    - 第二引数にオプションを渡す
        - `data` オプションに、グリッドに表示するデータを渡す

### Handsontableとの違い
* Jspreadsheet は Jsuites に依存しているため、読み込みが必要となります
* 行ヘッダーと列ヘッダーがデフォルト表示になっています

# データソース
jspreadsheet では、配列やオブジェクトなど幾つかのデータを取り込むことができる。

## 配列
```js
let data = [
  [ '佐藤', 28 ],
  [ '鈴木', 19 ],
  [ '田中', 25 ]
];

let grid = document.getElementById('grid');

new jspreadsheet(grid, {data: data});
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/f044e893-0942-cd4b-97c6-6c656bd5bdb8.png)

- 二次元配列を使用した最も基本的な方法

## オブジェクト
```js
let data = [
  { name: '佐藤', age: 25, a: 'A' },
  { name: '鈴木', age: 11, b: 'B' },
  { name: '田中', age: 21 }
];

let grid = document.getElementById('grid');

new jspreadsheet(grid, {data: data});
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/d10de47c-7cd6-d645-0ed5-79bcf662f21a.png)

- オブジェクトの配列も渡すことができる
- 各プロパティから値が取得され、グリッドに表示される
- どのプロパティが使用されるかは、先頭のオブジェクトで決まる模様

## ネストされたオブジェクト
```js
let data = [
  { name: {first: '佐藤', last: '一郎'}, age: 25 },
  { name: {first: '鈴木', last: '二郎'}, age: 11 },
  { name: {first: '田中', last: '三郎'}, age: 21 }
];

let grid = document.getElementById('grid');

new jspreadsheet(grid, {
  data: data,
  columns: [ // ★列のマッピングを定義できる
    { name: 'age' },
    { name: 'name.last' },
    { name: 'name.first' }
  ]
})
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/481e4981-7c46-7314-6899-9a29d4ab84b4.png)

- `column` オプションで、列ごとの細かい定義を宣言できる
- `column` の `data` オプションで、その列に表示する値をオブジェクトのどのプロパティから取得するかを指定できる
- ネストされたオブジェクトの場合は `name.first` のようにドット区切りでプロパティはCE版では指定できません

:::note info
ネストされたオブジェクトは、Pro版では指定可能です。
https://jspreadsheet.com/docs/data
:::

### Handsontableとの違い
MIT(6.2.2)版でも、ネストされたオブジェクトの場合は `name.first` のようにドット区切りでプロパティを指定できる。

## 任意のオブジェクトを使う
Jspreadsheetには`dataSchema`属性はない。

CE版では非対応
[Handsontable - 任意のオブジェクトを使う](https://qiita.com/opengl-8080/items/ac19deec388c357cd498#%E3%83%87%E3%83%BC%E3%82%BF%E3%82%BD%E3%83%BC%E3%82%B9)

# 空のグリッドを作る
```js
let grid = document.getElementById('grid');

new jspreadsheet(grid, {
  data: [{}],
  columns: [
    { name: 'name' },
    { name: 'age' }
  ],
  minSpareRows: 1
})
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/777a1b8f-baa2-1e05-bb03-4544bf9aa15c.png)

- `columns` で、列の順序を指定する
- `data` に空のデータ配列を渡す
- `minSpareRows` に最低限表示する行数を設定する

## 指定数の空のグリッドを作る
`minDimensions` に指定した行列数分の空グリッドを設定できる

```js
let grid = document.getElementById('grid');

new jspreadsheet(grid, {
  minDimensions: [4,3]
})
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/d8e3b06b-8e7b-bde3-741f-d1f54d9564cb.png)

### Handsontableとの違い
Jspreadsheetには`dataSchema`属性はないため、`data: []` を `data: [{}]` に変更

# データとグリッドを切り離す
グリッドを再描画してもデータの変更が反映されないようにしたい場合は、コンストラクタ引数で渡す `data` をディープコピーにする方法がある。

ディープコピーを作る手段の１つとして、 JSON に一旦エンコードして、 JavaScript オブジェクトにデコードし直す方法がある。

```js
let data = [
  [ '佐藤', 28 ],
  [ '鈴木', 19 ],
  [ '田中', 25 ]
];

let grid = document.getElementById('grid');

let table = new jspreadsheet(grid, {
  // JSON 文字列にしてから JavaScript オブジェクトに戻す
  data: JSON.parse(JSON.stringify(data))
});

data.push([ '山田', 22 ]);

table.updateTable();
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/bede62f1-1cc9-ec20-0baf-7d5957abfab1.png)

### Handsontableとの違い
* `table.render()` から `table.updateTable()` に変更
* `table.updateTable()`は描画のみで、`table.setData(data)`としないとデータは更新されない

# プラグイン
調査中

https://jspreadsheet.com/docs/plugins

### Handsontableとの違い
コメントはプラグインなしで標準機能として使用できる。

```js
let grid = document.getElementById('grid');

let table = new jspreadsheet(grid, {
  minDimensions: [5,5],
  allowComments: true
});

table.setComments([1, 1], 'This is a comment');
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/a7a13554-ed9f-3ffa-eec7-46109b213e4b.png)

# Hook（イベントハンドリング）
## イベントハンドラを追加する
```js
let grid = document.getElementById('grid');

let table = new jspreadsheet(grid, {
  minDimensions: [5,5]
});

table.options.onselection = function(el, startRow, startCol, endRow, endCol) {
  console.log(`${startRow}, ${startCol}, ${endRow}, ${endCol}`);
};
```
![onselection.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/28347304-945a-9ea5-2279-18ca5d678c88.gif)

- `options.(指定イベント)`でイベントハンドラを登録できる

### Handsontableとの違い
* `hooks`が無いため、`options`を使用する
* 複数の jspreadsheet インスタンスによる全てのグリッドのイベントは監視できない、特定のグリッドのみ
* `afterSelection` イベントはないため、`onselection` イベントに変更

### イベントの種類
[公式ドキュメント](https://bossanova.uk/jspreadsheet/v4/docs/events) を参照

* onafterchanges(records)：変更後
* onbeforechange(el, cell, x, y, value)：変更前
* oneditionend(el, cell, x, y, value, save)：編集終了時
* oneditionstart(el, cell, x, y)：編集開始時
* onbeforesave(el, obj, data)：保存前
* onsave(el, obj, data)：保存時
* onchange(el, cell, x, y, value, oldValue)：変更時
* onselection(el, borderLeft, borderTop, borderRight, borderBottom, origin)：選択時
* onsort(el, column, order)：ソート時
* onbeforeinsertcolumn(el, columnNumber, numOfColumns, insertBefore)：カラム挿入前
* oninsertcolumn(el, columnNumber, numOfColumns, historyRecords, insertBefore)：カラム挿入時
* onbeforedeletecolumn(el, columnNumber, numOfColumns)：カラム削除前
* ondeletecolumn(el, columnNumber, numOfColumns, historyRecords)：カラム削除時
* onmovecolumn(el, o, d)：カラム移動時
* onresizecolumn(el, column, width, oldWidth)：カラムサイズ変更時
* onbeforeinsertrow(el, rowNumber, numOfRows, insertBefore)：行挿入前
* oninsertrow(el, rowNumber, numOfRows, rowRecords, insertBefore)：行挿入時
* onbeforedeleterow(el, rowNumber, numOfRows)：行削除前
* ondeleterow(el, rowNumber, numOfRows, rowRecords)：行削除時
* onmoverow(el, o, d)：行移動時
* onresizerow(el, row, height, oldHeight)：行サイズ変更時
* onchangeheader(el, column, oldValue, newValue)：ヘッダ変更時
* onchangemeta(el, o, k, v)：メタ情報変更時
* onchangepage(el, pageNumber, oldPage)：ページ変更時
* onchangestyle(el, o, k, v)：スタイル変更時
* oncomments(el, comments, title, cell, cell[0], cell[1])：コメント変更時
* oncreateeditor(el, cell, x, y, editor)：エディタ作成時
* onblur(el)：フォーカスを失う時
* onfocus(el)：フォーカスがあたる時
* onload(el, obj)：ロード時
* onmerge(el, cellName, colspan, rowspan)：マージ時
* oncopy(el, row, hashString)：コピー時
* onbeforepaste(el, data, x, y)：ペースト前
* onpaste(el, data)：ペースト時
* onundo(el, historyRecord)：アンドゥ時
* onredo(el, historyRecord)：リドゥ時

## イベントハンドラを削除する
```js
let grid = document.getElementById('grid');

let table = new jspreadsheet(grid, {
  minDimensions: [5,5]
});

table.options.onselection = function(el, startRow, startCol, endRow, endCol) {
  console.log(`${startRow}, ${startCol}, ${endRow}, ${endCol}`);
};

document.getElementById('remove').addEventListener('click', function () {
  table.options.onselection = null;
});
```

![removehook.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/0dadd1d9-e8ff-a457-2c62-0bad3cc33324.gif)

- `options.(指定イベント)` に null でイベントハンドラを削除できる

## 登録されているイベントハンドラを取得する
Handsontableの`hooks.getBucket` メソッドと同じものは存在しないため、個別にメソッドを作成する必要がある。

## 一度だけ実行されるイベントハンドラを登録する
Handsontableの`hooks.once` メソッドと同じものは存在しないため、個別にメソッドを作成する必要がある。

## 任意のイベントを発火させる
Handsontableの`hooks.run` メソッドと同じものは存在しないため、個別にメソッドを作成する必要がある。
カスタムイベントを直接作成する機能は標準で提供されていないため、JavaScript の標準機能を利用してカスタムイベントを作成する。

```js
let grid = document.getElementById('grid');

let table = new jspreadsheet(grid, {
  minDimensions: [5,5]
});

// 独自のイベントハンドラを登録
table.el.addEventListener('onButtonClick', function(e) {
  console.log('click!!');
});

document.getElementById('button').addEventListener('click', function () {
  // イベントを発火
  table.el.dispatchEvent(new CustomEvent('onButtonClick'));
});
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/1e1aed20-ee1a-311e-ba63-85cd68eb7d40.png)

### イベントハンドラに値を渡す
detail プロパティに、渡したい任意の値を含めることで実現しています。
ここでは、foo と bar という値を含めています。

```js
let grid = document.getElementById('grid');

let table = new jspreadsheet(grid, {
  minDimensions: [5,5]
});

// 独自のイベントハンドラを登録
table.el.addEventListener('onButtonClick', function(e) {
  console.log('click!!', e.detail);
});

document.getElementById('button').addEventListener('click', function () {
  // イベントを発火
  table.el.dispatchEvent(
    new CustomEvent('onButtonClick',{ detail: { foo: 'Hello', bar: 'World' }})
  );
});
```

```text:実行結果
click!! ▶︎ {foo: 'Hello', bar: 'World'}
```

### キーコード
```js
let grid = document.getElementById('grid');

let table = new jspreadsheet(grid, {
  minDimensions: [5,5],
});

// キーダウンのイベントハンドラを登録
table.el.addEventListener('keydown', function(e) {
  if (e.keyCode === KEY_CODES.SPACE) {
    console.log('space!');
  } else if (isKey(e.keyCode, 'ARROW_LEFT|ARROW_RIGHT')) {
    console.log('left or right!!');
  }
});

const KEY_CODES = {
  ARROW_LEFT: 37,
  ARROW_RIGHT: 39,
  SPACE: 32
}

function isKey(keyCode, baseCode) {
  const keys = baseCode.split('|');
  let result = false;
  
  keys.forEach((key) => {
    if (keyCode === KEY_CODES[key]) {
      result = true;

      return false;
    }
  });
  
  return result;
}
```

![keycode.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/648d8153-9ebf-ac93-eca4-30625fe16340.gif)

- `beforeKeyDown`イベントがないため、`keydown`イベントを登録して使用する
- `KEY_CODES` に、よく使いそうなキーコードを定義する
- `isKey()` メソッドを使えば、 `|` 区切りで複数のキーのいずれかが入力されたことをチェックできる

### Handsontableとの違い
* key イベント関連は定義されていない
* Jspreadsheet には、Handsontable のような`KEY_CODES`が設定されていない
よって、Handsontable の定義を参照
https://github.com/handsontable/handsontable/blob/master/handsontable/src/helpers/unicode.js 

# Editor
調査中、これが分かれば Jsuites のような部品を作るようになりそうです。

# 最後に
思っている以上に Handsontable なら存在するメソッドが不足している気がします。
参考になりそうなコードも少ないため、解析しながら作成していく必要がありますね。