編集可能な簡易グリッドビュー画面をイチから作ってみる

直接関係はありませんが、これ の続きです。

ここにきて、カンタンなグリッドを表示する仕事があって、それならセルの内容を書き換えるだけの簡易的なセル編集機能もあればなぁ、と家に帰ってから唐突に思いつき、テレビを見ながら数時間ぐらいで突貫工事で作った。

仕様的には・・・
(1)Excelファイル(もしくはUTF-8なCSVファイル)をブラウザに読込み、
(2)簡単なグリッドビュー表示を行い、
(3)セルをクリックしたら編集し、
(4)リターンキー押下で編集確定、
(5)エスケープキーで編集取消
という感じ。

これならHTMLとCSS、JavaScriptで百数十行ぐらいでいけそうです。
実際業務で使うには、変更内容を追跡し、サーバーへ送信してデータベースに反映して・・・というような事が考えられますが・・・そこまでやっとれん!w

まずは、見てくれ、画面デザインですが、あくまでサンプル的なものなので、ほとんど素のHTMLです。
CSSのGridなんて初めて書いてみました。普段はBootstrapのレイアウトグリッドを利用して画面を作っているんですが・・・CSSのグリッドって、なんか難解ですね・・・ドキュメントを一回読んだだけではオッサンには理解できません(笑) エクセルの読込には、みんな大好き SheetJS ライブラリを利用しました。

ただ、セルを結合しまくったりしてるような複雑なエクセルファイルとか読み込むとたぶん滅茶苦茶になります。そういう複雑なエクセルファイルを持っていないので試していないんだけど・・・

デモ画面はこちら

CSS自体はそんなに凝ったことしてないので、読めば大体やろうとしていることが分かるでしょう。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style type="text/css">
      /* グリッドコンテナ */
      #excel-grid {
        height: 500px;
        overflow: auto;
        font-size: .8rem;
        position: relative;
      }

      /* グリッドの各行のスタイル */ 
      #excel-grid > .row {
        display: grid;
        column-gap: 0px;
        row-gap: 0px;
        border-top: 1px solid #999;
        border-left: 1px solid #999;
      }

      /* グリッドヘッダの固定(position: sticky) */
      .row.head {
        font-weight: bold;
        border-bottom: 2px solid #ccc !important;
        position: -webkit-sticky !important;
        position: sticky !important;
        z-index: 2 !important;
        top:0 !important;
        background-color: #544a65;
        color:white;
      }

      /* セルのスタイル */
      .row > .cell {
        height: 1rem;
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
        border-bottom: 1px solid #999;
        border-right: 1px solid #999;
        display: block;
        padding: 5px;
      }

      /* グリッドヘッダのセル部分 */
      .head > .cell {
        padding: 10px 5px !important;
      }

      /*セル編集時のフォーカス表示*/
      .row > .cell.editing {
        outline: 3px solid #bdceff;
        box-shadow: 0 0 10px 2px royalblue;
      }

    </style>
    <!-- jQuery,SheetJS ライブラリのロード -->
    <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.15.5/xlsx.full.min.js"></script>
  </head>

  <body>
    <h1 class="header">
      <span>超手抜きグリッドビュー</span>
      <input type="file" id="select-file">
    </h1>

    <!-- グリッドエリア -->
    <div id="excel-grid">
      <div class="row head"></div>
      <div class="row body"></div>
    </div>

    <!-- スクリプト本体は下記参照 -->
    <script type="text/javascript" src="./show-sheet.js"></script>
  </body>
</html>

続いて、肝心のJavaScript ですが、ほとんど各種イベントの処理ぐらいで、これといって特筆するべき事はありません。
ところどころ jQuery 使って見苦しいのですが・・・jQueryだと明らかにタイプ量が減るので僕は多用しています。世間の流れとしてはjQueryは排除されつつあるのですが・・・やっぱりjQueryに慣れきってしまうと document.querySelector なんてタイプするのがすんげぇ面倒。jQueryだと $ の一文字ですよ?イベント登録も on の二文字ですよ? addEventListener とかタイプするのがアホらしくなりますけどね、オッサンは。

/*******************************************************************************
 ミッション:
 ローカルのエクセルファイルを選択して疑似グリッドビュー編集を行う。
*******************************************************************************/
(function($,undefined) {

  // エクセルファイルロードのトリガー
  $('input#select-file[type=file]').on('change',on_input_type_file_change);
  
  // グリッドセルのイベントハンドラ登録
  $('.row.body')
    .on('click','.cell',on_cell_click)
    .on('blur','.cell',on_cell_blur)
    .on('keydown','.cell',on_cell_keydown);

  // ウィンドウサイズ変更のイベントハンドラ登録
  $(window).on('resize',on_window_resize).resize();


  /************************************************************************
   * 以下関数定義
  ************************************************************************/

  // グリッドの高さをウィンドウに合わせて調整
  function on_window_resize(e)
  {
    var h1 = $('.header').outerHeight(true);
    var h2 = $(window).height();
    var h3 = parseInt($(document.body).css('margin-top')) + parseInt($(document.body).css('margin-bottom')); 
    var h = h2 - h1 - h3;

    //画面下まで高さを伸ばす
    $('#excel-grid').height(h);
  }

  // エクセルファイルの読込完了ハンドラ
  function on_excel_file_load(e)
  {
    // お約束
    var data = new Uint8Array(e.target.result);
    var workbook = XLSX.read(data, {type: 'array'});

    // 一番最初のシートの名前を得る
    var n = workbook.SheetNames[0];

    // シート名からワークシートオブジェクトをゲト
    var ws = workbook.Sheets[n];

    // シートの全範囲を取得
    // ワークシートオブジェクトの各セルは、エクセルでよく使うA1形式でアクセスできます(ws['A1'], ws['A2']みたいに)
    // そして !ref プロパティーには A1:Z10のような形式でセルが使用されている範囲が入っています。
    // プログラムで利用するにはメンドクサイのでこの形式を一旦 row/column形式にデコードする必要があります。
    // ワークシートオブジェクトについては https://github.com/sheetjs/sheetjs/#sheet-objects を参照。
    var range = XLSX.utils.decode_range(ws['!ref']);

    // 使用されている最大列数を取得
    // デコードされた範囲オブジェクトrange は 
    // { s: { r: 数字, c: 数字 }, e: { r: 数字, c: 数字 } }
    // というような構造になっているようです。
    // s は start , e は end , r は row , c は column の略だと思われます。 
    var rownum = range.e.c + 1;

    // グリッド内のセルを初期化
    $('.row.head,.row.body').empty();

    // 最大列数から CSS のgrid-template-columnsプロパティの値を決めてあげます。
    // grid-template-columns: repeat(x,1fr); ここで x は rownumにする
    $('.row.head,.row.body').css('grid-template-columns','repeat(' + rownum.toString() + ',1fr)');

    // ワークシートをJSONデータに変換
    // 第二引数のオブジェクトで headerプロパティを与えていますが、
    // これは 各行をオブジェクト(object)ではなく配列(array)に変換するオプションです。
    // このオプションの意味は https://github.com/sheetjs/sheetjs#user-content-json を参照。
    var json = XLSX.utils.sheet_to_json(ws,{header:1})

    // <table>要素を使えばもっと簡単になったけど、敢えてイバラの道を進みます。
    var $head = $('#excel-grid > .head');
    var $body = $('#excel-grid > .body');

    // JSONデータから グリッドのセルを埋めていきます。
    $.each(json,function(i,arr) {
      var to = i == 0 ? $head : $body;
      for(var j=0;j<rownum;j++)
      {
        var v = arr[j] ? arr[j].toString() : '';
        var celldata = {'r':i,'c':j,'v':v};
        var attrs = {
          'data-cell': JSON.stringify(celldata),
          'title': v
        };
        $('<span>')
          .addClass('cell')
          .attr(attrs)
          .text(v)
          .appendTo(to);
      }
    });
  };

  // input[type=file]の onchange ハンドラ
  function on_input_type_file_change(e)
  {
    var files = this.files;
    var f = files[0];
    var reader = new FileReader();

    reader.onload = on_excel_file_load;
    reader.readAsArrayBuffer(f);
  }

  // 各セルをクリックした時のイベントハンドラ
  function on_cell_click(e)
  {
    e.preventDefault();
    e.stopPropagation();
    // セルをクリックしたらセルのclassにeditingを追加
    $(this).addClass('editing');

    // セルの元の値を undoプロパティに保存
    this.undo = this.textContent;

    // セルのスタイルを変更
    this.setAttribute('style','text-overflow: clip;');

    // セルを編集可能にする
    this.setAttribute('contentEditable','true');

    // フォーカスをあてる
    this.focus();
  }

  // セル編集中にフォーカスが外れたら cell_value_update関数をコールして更新
  function on_cell_blur(e)
  {
    if('true' === this.getAttribute('contentEditable'))
      cell_value_update(this);
  }

  // セルのonkeydownイベントハンドラ
  function on_cell_keydown(e)
  {
    // セル編集中、リターン(エンター)キーとエスケープキーをトラップ
    if(e.keyCode == 13 || e.keyCode == 27)
    {
      e.preventDefault();

      // エスケープキーなら復帰
      if(e.keyCode == 27)
        this.textContent = this.undo;

      cell_value_update(this);
    }
  }

  // cell 更新処理
  function cell_value_update(cell)
  {
    // セルの値が保存されていた編集前の値と違っていたら更新
    if(cell.textContent !== cell.undo)
    {
      var o = JSON.parse($(cell).attr('data-cell'));
      o.v = $(cell).text();
      $(cell).attr('data-cell',JSON.stringify(o));
    }

    // バックアップを廃棄
    delete cell.undo;

    // 編集を終了
    $(cell)
      .removeAttr('contentEditable')
      .removeAttr('style')
      .removeClass('editing')
      .scrollLeft(0);
  }

})(jQuery);

いつも利用させてもらってる疑似テストデータを利用してエクセルデータを流し込んでやると、こんな感じです。「こんな感じです」って言われてもね。。。

いちおう、エクセルファイルから簡易グリッドビューにデータを流し込み最低限の編集はできるようになりました。
・・・が、グリッドのHTML的な構造は見直す必要がありますよねぇ。

<div class="row body">
  <span class="cell"></span> ....延々とセルが続く
</div>

一つの要素(div.row.body)にすべてのセル(span.cell)を全部詰め込んで無理くりCSSでグリッド表示させてるだけなんで、<table>要素みたいに、行で分かれていないので、任意の行を選択したいときは、いちいち計算しないといけません。

ま、ちょっとだけグリッド表示したい、って時に使う用、と割り切る。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください