All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jwic.lib.slickgrid.plugins.slick.rowdetailview.js Maven / Gradle / Ivy

There is a newer version: 5.3.43
Show newest version
/***
 * A plugin to add row detail panel
 * Original StackOverflow question & article making this possible (thanks to violet313)
 * https://stackoverflow.com/questions/10535164/can-slickgrids-row-height-be-dynamically-altered#29399927
 * http://violet313.org/slickgrids/#intro
 * 
 *
 * USAGE:
 *
 * Add the slick.rowDetailView.(js|css) files and register the plugin with the grid.
 *
 * AVAILABLE ROW DETAIL OPTIONS:
 *    cssClass:         A CSS class to be added to the row detail
 *    loadOnce:         Booleang flag, when True will load the data once and then reuse it.
 *    preTemplate:      Template that will be used before the async process (typically used to show a spinner/loading)
 *    postTemplate:     Template that will be loaded once the async function finishes
 *    process:          Async server function call
 *    panelRows:        Row count to use for the template panel
 *    useRowClick:      Boolean flag, when True will open the row detail on a row click (from any column), default to False
 * 
 * AVAILABLE PUBLIC OPTIONS:
 *    init:                 initiliaze the plugin
 *    destroy:              destroy the plugin and it's events
 *    collapseAll:          collapse all opened row detail panel
 *    getColumnDefinition:  get the column definitions 
 *    getOptions:           get current plugin options
 *    setOptions:           set or change some of the plugin options
 * 
 * THE PLUGIN EXPOSES THE FOLLOWING SLICK EVENTS:
 *    onAsyncResponse:  This event must be used with the "notify" by the end user once the Asynchronous Server call returns the item detail 
 *       Event args:
 *          itemDetail: Item detail returned from the async server call
 *          detailView:  An explicit view to use instead of template (Optional)
 *
 *    onAsyncEndUpdate: Fired when the async response finished
 *       Event args:
 *         grid:        Reference to the grid.
 *         itemDetail:  Column definition.
 * 
 *    onBeforeRowDetailToggle: Fired before the row detail gets toggled
 *       Event args:
 *         grid:        Reference to the grid.
 *         item:  Column definition.
 * 
 *    onAfterRowDetailToggle: Fired after the row detail gets toggled
 *       Event args:
 *         grid:        Reference to the grid.
 *         item:  Column definition.
 *
 */
(function ($) {
  // register namespace
  $.extend(true, window, {
    "Slick": {
      "Plugins": {
        "RowDetailView": RowDetailView
      }
    }
  });


  function RowDetailView(options) {
    var _grid;
    var _self = this;
    var _expandedRows = [];
    var _handler = new Slick.EventHandler();
    var _defaults = {
      columnId: "_detail_selector",
      cssClass: null,
      toolTip: "",
      width: 30
    };

    var _options = $.extend(true, {}, _defaults, options);

    function init(grid) {
      _grid = grid;
      _dataView = _grid.getData();

      // Update the minRowBuffer so that the view doesn't disappear when it's at top of screen + the original default 3
      _grid.getOptions().minRowBuffer = _options.panelRows + 3;

      _handler
        .subscribe(_grid.onClick, handleClick)
        .subscribe(_grid.onSort, handleSort)
		.subscribe(_grid.onScroll, handleScroll);

      _grid.getData().onRowCountChanged.subscribe(function () { _grid.updateRowCount(); _grid.render(); });
      _grid.getData().onRowsChanged.subscribe(function (e, a) { _grid.invalidateRows(a.rows); _grid.render(); });

      // subscribe to the onAsyncResponse so that the plugin knows when the user server side calls finished
      subscribeToOnAsyncResponse();
    }

    function destroy() {
      _handler.unsubscribeAll();
      _self.onAsyncResponse.unsubscribe();
      _self.onAsyncEndUpdate.unsubscribe();
      _self.onAfterRowDetailToggle.unsubscribe();
      _self.onBeforeRowDetailToggle.unsubscribe();
    }

    function getOptions(options) {
      return _options;
    }

    function setOptions(options) {
      _options = $.extend(true, {}, _options, options);
    }
  
    function handleClick(e, args) {
      // clicking on a row select checkbox
      if (_options.useRowClick || _grid.getColumns()[args.cell].id === _options.columnId && $(e.target).hasClass("detailView-toggle")) {
        // if editing, try to commit
        if (_grid.getEditorLock().isActive() && !_grid.getEditorLock().commitCurrentEdit()) {
          e.preventDefault();
          e.stopImmediatePropagation();
          return;
        }

        var item = _dataView.getItem(args.row);

        // trigger an event before toggling
        _self.onBeforeRowDetailToggle.notify({
          "grid": _grid,
          "item": item
        }, e, _self);

        toggleRowSelection(item);

        // trigger an event after toggling
        _self.onAfterRowDetailToggle.notify({
          "grid": _grid,
          "item": item
        }, e, _self);

        e.stopPropagation();
        e.stopImmediatePropagation();
      }
    }

    // Sort will just collapse all of the open items
    function handleSort(e, args) {
      collapseAll();
    }

	// If we scroll save detail views that go out of cache range
    function handleScroll(e, args) {
        
        var range = _grid.getRenderedRange();

        var start = (range.top > 0 ? range.top : 0);
        var end = (range.bottom > _dataView.getLength() ? range.bottom : _dataView.getLength());
        
        // Get the item at the top of the view
        var topMostItem = _dataView.getItemByIdx(start);

        // Check it is a parent item
        if (topMostItem._parent == undefined)
        {
            // This is a standard row as we have no parent.
            var nextItem = _dataView.getItemByIdx(start + 1);
            if(nextItem !== undefined && nextItem._parent !== undefined)
            {
                // This is likely the expanded Detail Row View
                // Check for safety
                if(nextItem._parent == topMostItem)
                {
                    saveDetailView(topMostItem);
                }
            }
        }

        // Find the bottom most item that is likely to go off screen
        var bottomMostItem = _dataView.getItemByIdx(end - 1);

        // If we are a detailView and we are about to go out of cache view
        if(bottomMostItem._parent !== undefined)
        {
            saveDetailView(bottomMostItem._parent);
            
        }
    }
	
    // Toggle between showing and hiding a row
    function toggleRowSelection(row) {
      _grid.getData().beginUpdate();
      HandleAccordionShowHide(row);
      _grid.getData().endUpdate();
    }

    // Collapse all of the open items
    function collapseAll() {
      for (var i = _expandedRows.length - 1; i >= 0; i--) {
        collapseItem(_expandedRows[i]);
      }
    }
	
    // Saves the current state of the detail view
    function saveDetailView(item)
    {
        var view = $("#innerDetailView_" + item.id);
        if (view)
        {
            var html = $("#innerDetailView_" + item.id).html();
            if(html !== undefined)
            {
                item._detailContent = html;
            }
        }   
    }
	
    // Colapse an Item so it is notlonger seen
    function collapseItem(item) {
	  
      // Save the details on the collapse assuming onetime loading
      if (_options.loadOnce) {
          saveDetailView(item);
      }
		
      item._collapsed = true;
      for (var idx = 1; idx <= item._sizePadding; idx++) {
        _dataView.deleteItem(item.id + "." + idx);
      }
      item._sizePadding = 0;
      _dataView.updateItem(item.id, item);

      // Remove the item from the expandedRows
      _expandedRows = _expandedRows.filter(function (r) {
        return r.id !== item.id;
      });
    }

    // Expand a row given the dataview item that is to be expanded
    function expandItem(item) {
      item._collapsed = false;
      _expandedRows.push(item);
      
      // In the case something went wrong loading it the first time such a scroll of screen before loaded
      if (!item._detailContent) item._detailViewLoaded = false;	  
	   
      // display pre-loading template
      if (!item._detailViewLoaded || _options.loadOnce !== true) {
        item._detailContent = _options.preTemplate(item);
      } else {
        _self.onAsyncResponse.notify({
          "itemDetail": item,
          "detailView": item._detailContent
        }, undefined, this);
        applyTemplateNewLineHeight(item);
        _dataView.updateItem(item.id, item);

        return;
      }

      applyTemplateNewLineHeight(item);
      _dataView.updateItem(item.id, item);

      // async server call
      _options.process(item);
    }

    /**
     * subscribe to the onAsyncResponse so that the plugin knows when the user server side calls finished
     * the response has to be as "args.itemDetail" with it's data back
     */
    function subscribeToOnAsyncResponse() {      
      _self.onAsyncResponse.subscribe(function (e, args) {      
        if (!args || !args.itemDetail) {
          throw 'Slick.RowDetailView plugin requires the onAsyncResponse() to supply "args.itemDetail" property.'
        }

        // If we just want to load in a view directly we can use detailView property to do so
        if (args.detailView) {
          args.itemDetail._detailContent = args.detailView;
        } else {
          args.itemDetail._detailContent = _options.postTemplate(args.itemDetail);
        }

        args.itemDetail._detailViewLoaded = true;

        var idxParent = _dataView.getIdxById(args.itemDetail.id);
        _dataView.updateItem(args.itemDetail.id, args.itemDetail);

        // trigger an event once the post template is finished loading
        _self.onAsyncEndUpdate.notify({
            "grid": _grid,
            "itemDetail": args.itemDetail
        }, e, _self);
      });
    }

    function HandleAccordionShowHide(item) {
      if (item) {
        if (!item._collapsed) {
          collapseItem(item);
        } else {
          expandItem(item);
        }
      }
    }

    //////////////////////////////////////////////////////////////
    //////////////////////////////////////////////////////////////
    var getPaddingItem = function (parent, offset) {
      var item = {};

      for (var prop in _grid.getData()) {
        item[prop] = null;
      }
      item.id = parent.id + "." + offset;

      //additional hidden padding metadata fields
      item._collapsed = true;
      item._isPadding = true;
      item._parent = parent;
      item._offset = offset;

      return item;
    }

    //////////////////////////////////////////////////////////////
    //create the detail ctr node. this belongs to the dev & can be custom-styled as per
    //////////////////////////////////////////////////////////////
    function applyTemplateNewLineHeight(item) {
      // the height seems to be calculated by the template row count (how many line of items does the template have)
      var rowCount = _options.panelRows;

      //calculate padding requirements based on detail-content..
      //ie. worst-case: create an invisible dom node now &find it's height.
      var lineHeight = 13; //we know cuz we wrote the custom css innit ;)
      item._sizePadding = Math.ceil(((rowCount * 2) * lineHeight) / _grid.getOptions().rowHeight);
      item._height = (item._sizePadding * _grid.getOptions().rowHeight);

      var idxParent = _dataView.getIdxById(item.id);
      for (var idx = 1; idx <= item._sizePadding; idx++) {
        _dataView.insertItem(idxParent + idx, getPaddingItem(item, idx));
      }
    }


    function getColumnDefinition() {
      return {
        id: _options.columnId,
        name: "",
        toolTip: _options.toolTip,
        field: "sel",
        width: _options.width,
        resizable: false,
        sortable: false,
        cssClass: _options.cssClass,
        formatter: detailSelectionFormatter
      };
    }

    function detailSelectionFormatter(row, cell, value, columnDef, dataContext) {

      if (dataContext._collapsed == undefined) {
        dataContext._collapsed = true,
          dataContext._sizePadding = 0,     //the required number of pading rows
          dataContext._height = 0,     //the actual height in pixels of the detail field
          dataContext._isPadding = false,
          dataContext._parent = undefined,
          dataContext._offset = 0
      }

      if (dataContext._isPadding == true) {
        //render nothing
      } else if (dataContext._collapsed) {
        return "
"; } else { var html = []; var rowHeight = _grid.getOptions().rowHeight; var bottomMargin = 5; //V313HAX: //putting in an extra closing div after the closing toggle div and ommiting a //final closing div for the detail ctr div causes the slickgrid renderer to //insert our detail div as a new column ;) ~since it wraps whatever we provide //in a generic div column container. so our detail becomes a child directly of //the row not the cell. nice =) ~no need to apply a css change to the parent //slick-cell to escape the cell overflow clipping. //sneaky extra
inserted here-----------------v html.push("
"); html.push("
"); //shift detail below 1st row html.push("
"); //sub ctr for custom styling html.push("
" , dataContext._detailContent, "
"); //&omit a final closing detail container
that would come next return html.join(""); } return null; } function resizeDetailView(item) { if (!item) return; // Grad each of the dom items var mainContainer = document.getElementById('detailViewContainer_' + item.id); var cellItem = document.getElementById('cellDetailView_' + item.id); var inner = document.getElementById('innerDetailView_' + item.id); if (!mainContainer || !cellItem || !inner) return; for (var idx = 1; idx <= item._sizePadding; idx++) { _dataView.deleteItem(item.id + "." + idx); } var rowHeight = _grid.getOptions().rowHeight; // height of a row var lineHeight = 13; //we know cuz we wrote the custom css innit ;) // Get the inner Item height as this will be the actual size var itemHeight = inner.clientHeight; // Now work out how many rows var rowCount = Math.ceil(itemHeight / rowHeight) + 1; item._sizePadding = Math.ceil(((rowCount * 2) * lineHeight) / rowHeight); item._height = (item._sizePadding * rowHeight); // If the padding is now more than the original minRowBuff we need to increase it if (_grid.getOptions().minRowBuffer < item._sizePadding) { // Update the minRowBuffer so that the view doesn't disappear when it's at top of screen + the original default 3 _grid.getOptions().minRowBuffer =item._sizePadding + 3; } mainContainer.setAttribute("style", "max-height: " + item._height + "px"); if (cellItem) cellItem.setAttribute("style", "height: " + item._height + "px;top:" + rowHeight + "px"); var idxParent = _dataView.getIdxById(item.id); for (var idx = 1; idx <= item._sizePadding; idx++) { _dataView.insertItem(idxParent + idx, getPaddingItem(item, idx)); } } $.extend(this, { "init": init, "destroy": destroy, "collapseAll": collapseAll, "getColumnDefinition": getColumnDefinition, "getOptions": getOptions, "setOptions": setOptions, "onAsyncResponse": new Slick.Event(), "onAsyncEndUpdate": new Slick.Event(), "onAfterRowDetailToggle": new Slick.Event(), "onBeforeRowDetailToggle": new Slick.Event(), "resizeDetailView": resizeDetailView }); } })(jQuery);




© 2015 - 2025 Weber Informatics LLC | Privacy Policy