jagridを支える技術

個人的には[axebomber-clj](https://github.com/kawasima/axebomber-clj/)のサブプロジェクトのつもりで作った[jagrid](https://github.com/kawasima/jagrid/)ですが、全国のExcelホウガンサーに喜んでいただけたようで、作ったかいがありました。

基本的には、position: absoluteで、絶対座標をmargin-top / margin-leftに変換するだけなのですが、細かいところで工夫してありますので、多少解説しておきます。

## garden

rubyにおけるSCSSと同じ問題領域に、clojureでは[garden](https://github.com/noprompt/garden)があります。
スタイルシートをS式で書けるスグレモノですが、現段階では引数をとる擬似クラスや子セレクタ(>)に対応する機能は無く、ちょっと物足りない感はあります。しかし、CSSをS式で書ける喜びは何物にも代えがたいですね。

```clj
 [:.jagrid
   {:border-width (px 0)
    :line-height (px cell-size)
    :position "relative"
    :border-style "solid"
    :padding-left (px cell-size)
    :overflow "hidden"}
    [:.jg-header
      {:background-color header-bgcolor
       :box-shadow "none"}
      [:&.row
        {:position "absolute"
         :margin-top  (px (- cell-size))
         :margin-left (px (- cell-size))}]
```

ちなみに、Javascriptをコンパクトにするために、現在はClojureScriptは使っていません。将来的には対応予定です。

## 背景のグリッド

現在は1マス20×20 ピクセル固定ですが、このサイズは変更できるようにするため、背景のグリッドはCanvasで動的に生成しています。

```js
function makeGridImage() {
    var gridSheet = document.createElement("canvas");
    gridSheet.setAttribute("width", CELL_SIZE + "px");
    gridSheet.setAttribute("height", CELL_SIZE + "px");

    var context = gridSheet.getContext("2d");
    context.moveTo(CELL_SIZE, 0);
    context.lineTo(CELL_SIZE, CELL_SIZE);
    context.moveTo(0, CELL_SIZE);
    context.lineTo(CELL_SIZE, CELL_SIZE);

    context.strokeStyle = "#dadcdd";
    context.stroke();
    return gridSheet.toDataURL("image/png")
}
```

ちなみに、現在は20ピクセルのセルの内側に罫線を引いています。これはExcelの仕様と異なる大きな問題なので、外側に引くように修正予定です。

## 方眼紙表組みの実装

[Excel方眼紙の代表作である「営業日報」の例](http://itpro.nikkeibp.co.jp/article/Watcher/20140321/545102/)を見ていただけるとわかるのですが、通常のHTMLのテーブルよりも、行ごとにセル幅が変わったりして、単なるcolspanでは結構煩雑なタグ構造になることが想像できるかと思います。

したがって、jagridは別途data-width属性を用意して、こちらでセルサイズをマス目の数で指定できるようにしてあります。そして、実装としてはひとマスが1つのtdになるようにしていて、data-widthのサイズ指定に合わせてcolspanに変換されるのです。

```js
  function toGridTable(tbl) {
        var tableWidth = 0;
        var rowspans = [];
        forEach.call(tbl.querySelectorAll("tr"), function (row) {
            var rowWidth = 0;
            forEach.call(row.querySelectorAll("td,th"), function (cell) {
                rowspans.push(cell.getAttribute("rowspan") || 1);
                rowWidth += parseInt(cell.getAttribute("data-width") || 0);
            });
            tableWidth = Math.max(tableWidth, rowWidth);
        });

        var widthAry = new Array(tableWidth);
        var rowspanAry = new Array(tableWidth);
        forEach.call(tbl.querySelectorAll("tr"), function (row) {
            var columnIndex = 0;
            forEach.call(row.querySelectorAll("td,th"), function (cell) {
                while (rowspanAry[columnIndex] > 1 && columnIndex < tableWidth) {
                    rowspanAry[columnIndex++]--;
                }
                var rowspan = parseInt(cell.getAttribute("rowspan") || 1);
                var w = parseInt(cell.getAttribute("data-width"));
                var h = parseInt(cell.getAttribute("data-height"));

                if (isNaN(w)) w = widthAry[columnIndex];
                for (var i = 0; i < w; i++) {
                    rowspanAry[columnIndex + i] = rowspan;
                    widthAry[columnIndex + i] = w;
                }
                columnIndex += w;

                cell.removeAttribute("data-width");
                cell.setAttribute("colspan", w);
                if (!isNaN(h)) cell.style.height = CELL_SIZE * h + "px";
                wrapAll(cell.childNodes, "div");
            });
        });
```

そうなると、今度はrowspan属性がそのままではレイアウトが崩れてしまうので、補正をかけているのが上記コードの部分になります。

ちなみに、セルをdivでwrapしているのは、文字が長い時にセルが伸びてしまうのを抑止するためで、かつExcel方眼紙でのテーブルの文字切れを再現するためのものでもあります。

## 今後の予定

よくあるグリッドライブラリと違って、あくまでも方眼紙レイアウトをサポートするものなので、全体が編集可能にすることは考えていません。が、formなどは対応する予定です。
このjagridが真価を発揮するのはaxebomber-cljと連携するときなので、理想のExcel方眼紙を追い求めているみなさんにおかれましてはwktkしてお待ちください。