ぼくの話すことは、全部ネットに書いてある。

週末引きこもり族がカメラ片手に外に出ようとするブログ。

PrimeFaces6.1コンポーネントDataTableハック[Java EE/Jakarta EE]

会社で所属部署が変わり、Webアプリケーションの開発を行うことになりました。
使用するのはJakarta EE(Java EE)+GlassFish+PrimeFaces

f:id:aino123:20180312195526p:plainどう見てもトランスフォーマーモチーフ。

業務でJava使うのは初(チームメンバーも案件で使ったことないという……)。

ハックとかそんな大層なものでもないけれど、日本語資料が少ないので記録として残しておきます。

PrimeFacesコンポーネント(DataTable)拡張

PrimeFacesのコンポーネントは便利な半面小回りが効きません。
デモでは単体で動作している機能も、複合して使おうとすると思わぬ競合で動かないときがあります。

そういった時、必要になるのがextend

例1:DataTableのバグを直してみる~ColumnToggler+CellEdit~

表の項目の表示非表示を切り替えられる「ColumnToggler」。
便利ですが、「Cell Edit」と組み合わせると、Tabキーによる移動の挙動がおかしくなります。
PrimeFacesはTab移動を「tabCell」というFunctionで実装していますが、非表示の項目にまでフォーカスを合わせてしまうからです。

修正してみます。

primefaces/datatable.js at 6_1 · primefaces/primefaces · GitHub

GitHubソースコードから、Functionをコピーして追記(バージョンに注意)。

if(PrimeFaces.widget.DataTable) {
    PrimeFaces.widget.DataTable = PrimeFaces.widget.DataTable.extend({
    tabCell: function(cell, forward) {
        var targetCell = forward ? cell.next() : cell.prev();
        if(targetCell.length == 0) {
            var tabRow = forward ? cell.parent().next() : cell.parent().prev();
            targetCell = forward ? tabRow.children('td.ui-editable-column:first') : tabRow.children('td.ui-editable-column:last');
        }
        if(targetCell.is('td.ui-helper-hiden')){ //☆隠されたセルに移動したら再帰
            this.tabCell(targetCell,forward);    //☆
        } else {                                 //☆
            this.showCellEditor(targetCell);
        }                                        //☆
    }
}

☆が追記行です。

私は、上記のようなコードを「primefaces-patches.js」として保存し、JSF側で「<h:outputScript name="primefaces-patches.js" library="js" target="head"/>」のように読み込ませています。

例2:CellEdit中に右クリックしても編集終了しないようにする

CellEditで地味に厄介なのが、テキストボックスの編集中に右クリックすると編集終了してしまうとこ。

コンテキストメニューを使っての編集ができません。
なので、入力領域内での右クリックで編集が終了しないようにします。

if(PrimeFaces.widget.DataTable) {
    PrimeFaces.widget.DataTable = PrimeFaces.widget.DataTable.extend({
    bindEditEvents: function(cell, forward) {
        this.super(); //☆一部の動作を上書きできればいいので、元Functionを呼ぶ。
        var $this = this; //☆
        if(this.cfg.editMode === 'cell') {
            var cellSelector = '> tr > td.ui-editable-column';
            
            this.tbody.off('click.datatable-cell', cellSelector)
                        .on('click.datatable-cell', cellSelector, null, function(e) {
                            $this.incellClick = true;
                            
                            var cell = $(this);
                            if(!cell.hasClass('ui-cell-editing')) {
                                $this.showCellEditor($(this));
                            }
                        });
                        
            $(document).off('click.datatable-cell-blur' + this.id)
                        .on('click.datatable-cell-blur' + this.id, function(e) {                            
                            if(!$this.incellClick && $this.currentCell && !$this.contextMenuClick && !$.datepicker._datepickerShowing) {
                                if($this.cfg.saveOnCellBlur) {          //☆
                                    if(!e.target.classList.contains("ui-inputfield") || e.button !== 2) //入力領域外での右クリックまたは左クリックなら編集終了
                                        $this.saveCell($this.currentCell);
                                }                                       //☆
                                else
                                    $this.doCellEditCancelRequest($this.currentCell);
                            }
                            
                            $this.incellClick = false;
                            $this.contextMenuClick = false;
                        });
}

ちなみに、セル編集中のcontextMenuと行選択時のcontextMenuを使い分けたい時は

<p:contextMenu for="table" beforeShow="return PF('theWidget').currentCell == null;">
    hogehoge
</p:contextMenu>

のようにすればOK。セルが編集中ならcurrentCellに値が入っています。

例3:CellEditにおいてEnterキー、矢印キーで上下移動する

Excelっぽい移動を実装します。

if(PrimeFaces.widget.DataTable) {
    PrimeFaces.widget.DataTable = PrimeFaces.widget.DataTable.extend({
    enterCell: function(cell, forward) {                                 //☆tabCellを元に新規作成
        var tabRow = forward ? cell.parent().next : cell.parent().prev();//☆移動方向判定
        var targetCell = tabRow.children('td.ui-editable-column:first'); //☆次の行の先頭を取得
        for(var i = 0;i < cell[0].cellIndex;i++) {                       //☆
            targetCell = targetCell.next();                              //☆
        }                                                                //☆
        this.showCellEditor(targetCell);                                 //☆
    },                                                                   //☆
    showCurrentCell: function(cell) {
        ~略~
        //bind events on demand
        if(!cell.data('edit-events-bound')) {
            cell.data('edit-events-bound', true);
            
            inputs.on('keydown.datatable-cell', function(e) {
                    var keyCode = $.ui.keyCode,
                    shiftKey = e.shiftKey,
                    key = e.which,
                    input = $(this);
                    
                    if(key === keyCode.ENTER || key == keyCode.NUMPAD_ENTER) {
                        $this.saveCell(cell);
                        
                        $this.enterCell(cell,!shiftKey);                 //☆
                        
                        e.preventDefault();
                    }
                    else if(key === keyCode.UP) {                        //☆
                        $this.saceCell(cell);                            //☆
                        $this.enterCell(cell,false);                     //☆
                        e.preventDefault();                              //☆
                    }                                                    //☆
                    else if(key === keyCode.DOWN) {                      //☆
                        $this.saceCell(cell);                            //☆
                        $this.enterCell(cell,true);                      //☆
                        e.preventDefault();                              //☆
                    }                                                    //☆
                    else if(key === keyCode.TAB) {
                        if(multi) {
                            var focusIndex = shiftKey ? input.index() - 1 : input.index() + 1;
                            
                            if(focusIndex < 0 || (focusIndex === inputs.length) || input.parent().hasClass('ui-inputnumber')) {
                                $this.tabCell(cell, !shiftKey);                                
                            } else {
                                inputs.eq(focusIndex).focus();
                            }
                        }
                        else {
                            $this.tabCell(cell, !shiftKey);
                        }
                        
                        e.preventDefault();
                    }
                    else if(key === keyCode.ESCAPE) {
                        $this.doCellEditCancelRequest(cell);
                        $this.currentCell = null;
                        e.preventDefault();
                    }
                })
                .on('focus.datatable-cell click.datatable-cell', function(e) {
                    $this.currentCell = cell;
                });
        }
    });
}

tabCellを元にenterCellを新規実装。
キーイベントを追加してます。

例4:CellEditで編集した行に色を付ける

DataTableにおいて、行ごとに色を付けるためにはrowStyleClassを用います。

<p;dataTable id="mytable" widgetVar="theWidget" value="#{Bb.view}" var="item" rowKey="#{item.id}"
  editable="true" editMode="cell" selectionMode="single" selection="#{Bb.selected}"
  rowStyleClass="#{Bb.update_item.contains(item) ? 'update' : null}">
    <p:ajax event="cellEdit" listner="#{Bb.onCellEdit}" update=":form:msgs" />
    <hogehoge />
</p:dataTable>

管理ビーンのonCellEditでは、ArrayListであるupdate_itemにselectedをAddしてます。
PrimeFacesのDataTableは編集時、編集が行われた行のみ再描画します。
しかし、rowStyleClassの判定を再実行するためには、DataTable自体の再描画が必要です。

しかしながら、テーブル全体を無理やり再描画してしまうと、再描画とcellEditイベントが競合してしまい、挙動がおかしくなることがあります。

そこで、cellEditRequest発行時にスタイルシートを適応します。

if(PrimeFaces.widget.DataTable) {
    PrimeFaces.widget.DataTable = PrimeFaces.widget.DataTable.extend({
    doCellEditRequest: function(cell) {
        this._super(cell);                 //☆
        cell.parent().addClass('update');  //☆
    });
}

ここで設定する'update'は再描画を行うと消えてしまいますが、再描画時にrowStyleClassの判定が行われることで設定し直されます。

参考資料

stackoverflow.com