text: /*!
   * ZUI: 数据表格 - v1.9.1 - 2020-02-05
   * http://zui.sexy
   * GitHub: https://github.com/easysoft/zui.git 
   * Copyright (c) 2020 cnezsoft.com; Licensed MIT
   */
  
  /* ========================================================================
   * ZUI: datatable.js
   * http://zui.sexy
   * ========================================================================
   * Copyright (c) 2014-2016 cnezsoft.com; Licensed MIT
   * ======================================================================== */
  (function($) {
      'use strict';
  
      var name = 'zui.datatable';
      var store = $.zui.store;
  
      /**
       * Datatable class
       *
       * @param object element           DOM element or jquery element
       * @param object options           Datatable options
       */
      var DataTable = function(element, options) {
          this.name = name;
          this.$ = $(element);
          this.isTable = (this.$[0].tagName === 'TABLE');
          this.firstShow = true;
          if(this.isTable) {
              this.$table = this.$;
              this.id = 'datatable-' + (this.$.attr('id') || $.zui.uuid());
          } else {
              this.$datatable = this.$.addClass('datatable');
              if(this.$.attr('id')) {
                  this.id = this.$.attr('id');
              } else {
                  this.id = 'datatable-' + $.zui.uuid();
                  this.$.attr('id', this.id);
              }
          }
          this.getOptions(options);
          this.load();
  
          this.callEvent('ready');
      };
  
      // default options
      DataTable.DEFAULTS = {
          // Check options
          checkable: false, // added check icon to the head of rows
          checkByClickRow: true, // change check status by click anywhere on a row
          checkedClass: 'active', // apply CSS class to an checked row
          checkboxName: null,
          selectable: true,
  
          // Sort options
          sortable: false, // enable sorter
  
          // storage
          storage: true, // enable storage
  
          // fixed header of columns
          fixedHeader: false, // fixed header
          fixedHeaderOffset: 0, // set top offset of header when fixed
          fixedLeftWidth: '30%', // set left width after first render
          fixedRightWidth: '30%', // set right width after first render
          flexHeadDrag: true, // scroll flexarea by drag header
          scrollPos: 'in', // scroll bar position: 'out' | 'in'
  
          // hover effection
          rowHover: true, // apply hover effection to row
          colHover: true, // apply hover effection to head
          hoverClass: 'hover',
          colHoverClass: 'col-hover',
  
  
          // Fix cell height
          fixCellHeight: true,
  
          // Merge rows
          mergeRows: false, // Merge rows
  
          // custom columns size
          // customizable: false, // enable customizable
          minColWidth: 20, // min width of columns
          minFixedLeftWidth: 200, // min left width
          minFixedRightWidth: 200, // min right width
          minFlexAreaWidth: 200 // min flexarea width
      };
  
      // Get options
      DataTable.prototype.getOptions = function(options) {
          var $e = this.$;
          options = $.extend({}, DataTable.DEFAULTS, this.$.data(), options);
  
          options.tableClass = options.tableClass || '';
          options.tableClass = ' ' + options.tableClass + ' table-datatable';
  
          $.each(['bordered', 'condensed', 'striped', 'condensed', 'fixed'], function(idx, cls) {
              cls = 'table-' + cls;
              if($e.hasClass(cls)) options.tableClass += ' ' + cls;
          });
  
          if($e.hasClass('table-hover') || options.rowHover) {
              options.tableClass += ' table-hover';
          }
  
          if(!options.checkable || !$.fn.selectable) options.selectable = false;
  
          this.options = options;
      };
  
      // Load data form options or table dom
      DataTable.prototype.load = function(data) {
          var options = this.options,
              cols;
  
          if($.isFunction(data)) {
              data = data(this.data, this);
              data.keepSort = true;
          } else if($.isPlainObject(data)) {
              this.data = data;
          } else if(typeof data === 'string') {
              var $table = $(data);
              if($table.length) {
                  this.$table = $table.first();
                  this.$table.data(name, this);
                  this.isTable = true;
              }
              data = null;
          } else {
              data = options.data;
          }
  
          if(!data) {
              if(this.isTable) {
                  data = {
                      cols: [],
                      rows: []
                  };
                  cols = data.cols;
                  var rows = data.rows,
                      i,
                      $th, $tr, $td, row, $t = this.$table,
                      colSpan;
  
                  $t.children('thead').children('tr:first').children('th').each(function() {
                      $th = $(this);
                      cols.push($.extend({
                          text: $th.html(),
                          flex: false || $th.hasClass('flex-col'),
                          width: 'auto',
                          cssClass: $th.attr('class'),
                          css: $th.attr('style'),
                          type: 'string',
                          ignore: $th.hasClass('ignore'),
                          sort: !$th.hasClass('sort-disabled'),
                          mergeRows: $th.attr('merge-rows'),
                          title: $th.attr('title')
                      }, $th.data()));
                  });
  
                  $t.children('tbody').children('tr').each(function() {
                      $tr = $(this);
                      row = $.extend({
                          data: [],
                          checked: false,
                          cssClass: $tr.attr('class'),
                          css: $tr.attr('style'),
                          id: $tr.attr('id')
                      }, $tr.data());
  
                      $tr.children('td').each(function() {
                          $td = $(this);
                          colSpan = $td.attr('colspan') || 1;
                          row.data.push($.extend({
                              cssClass: $td.attr('class'),
                              css: $td.attr('style'),
                              text: $td.html(),
                              colSpan: colSpan,
                              title: $td.attr('title')
                          }, $td.data()));
  
                          if(colSpan > 1) {
                              for(i = 1; i < colSpan; i++) {
                                  row.data.push({
                                      empty: true
                                  });
                              }
                          }
                      });
  
                      rows.push(row);
                  });
  
                  var $tfoot = $t.children('tfoot');
                  if($tfoot.length) {
                      data.footer = $('<table class="table' + options.tableClass + '"></table>').append($tfoot);
                  }
              } else {
                  throw new Error('No data avaliable!');
              }
          }
  
          data.flexStart = -1;
          data.flexEnd = -1;
  
          cols = data.cols;
          data.colsLength = cols.length;
          for(var i = 0; i < data.colsLength; ++i) {
              var col = cols[i];
              if(col.flex) {
                  if(data.flexStart < 0) {
                      data.flexStart = i;
                  }
  
                  data.flexEnd = i;
              }
          }
  
          if(data.flexStart === 0 && data.flexEnd === data.colsLength) {
              data.flexStart = -1;
              data.flexEnd = -1;
          }
  
          data.flexArea = data.flexStart >= 0;
          data.fixedRight = data.flexEnd >= 0 && data.flexEnd < (data.colsLength - 1);
          data.fixedLeft = data.flexStart > 0;
          if(data.flexStart < 0 && data.flexEnd < 0) {
              data.fixedLeft = true;
              data.flexStart = data.colsLength;
              data.flexEnd = data.colsLength;
          }
  
          this.data = data;
  
          this.callEvent('afterLoad', {
              data: data
          });
  
          this.render();
      };
  
      // Render datatable
      DataTable.prototype.render = function() {
          var that = this;
          var $datatable = that.$datatable || (that.isTable ? $('<div class="datatable" id="' + that.id + '"/>') : that.$datatable),
              options = that.options,
              data = that.data,
              cols = that.data.cols,
              rows = that.data.rows;
          var checkable = options.checkable,
              $left,
              i,
              $right,
              $flex,
              dataRowSpan = '<div class="datatable-rows-span datatable-span"><div class="datatable-wrapper"><table class="table"></table></div></div>',
              dataHeadSpan = '<div class="datatable-head-span datatable-span"><div class="datatable-wrapper"><table class="table"><thead></thead></table></div></div>';
  
          $datatable.children('.datatable-head, .datatable-rows, .scroll-wrapper').remove();
  
          // Set css class to datatable by options
          $datatable.toggleClass('sortable', options.sortable);
          // $datatable.toggleClass('customizable', options.customizable);
  
          // Head
          var $head = $('<div class="datatable-head"/>'),
              $tr,
              $th,
              col;
          $left = $('<tr class="datatable-row datatable-row-left"/>');
          $right = $('<tr class="datatable-row datatable-row-right"/>');
          $flex = $('<tr class="datatable-row datatable-row-flex"/>');
          for(i = 0; i < cols.length; i++) {
              col = cols[i];
              $tr = i < data.flexStart ? $left : ((i >= data.flexStart && i <= data.flexEnd) ? $flex : $right);
              if(i === 0 && checkable) {
                  $tr.append('<th data-index="check" class="check-all check-btn"><i class="icon-check-empty"></i></th>');
              }
              if(col.ignore) continue;
  
              $th = $('<th class="datatable-head-cell"/>');
  
              // set sort class
              $th.toggleClass('sort-down', col.sort === 'down')
                  .toggleClass('sort-up', col.sort === 'up')
                  .toggleClass('sort-disabled', col.sort === false);
  
              $th.addClass(col.cssClass)
                  .addClass(col.colClass)
                  .html(col.text)
                  .attr({
                      'data-index': i,
                      'data-type': col.type,
                      style: col.css,
                      title: col.title,
                  }).css('width', col.width);
              $tr.append($th);
          }
  
          var $headSpan;
          if(data.fixedLeft) {
              $headSpan = $(dataHeadSpan);
              $headSpan.addClass('fixed-left')
                  // .find('.datatable-wrapper')
                  // .append('<div class="size-handle size-handle-head size-handle-left"></div>')
                  .find('table')
                  .addClass(options.tableClass)
                  .find('thead').append($left);
              $head.append($headSpan);
          }
          if(data.flexArea) {
              $headSpan = $(dataHeadSpan);
              $headSpan.addClass('flexarea')
                  .find('.datatable-wrapper')
                  .append('<div class="scrolled-shadow scrolled-in-shadow"></div><div class="scrolled-shadow scrolled-out-shadow"></div>')
                  .find('table')
                  .addClass(options.tableClass)
                  .find('thead').append($flex);
              $head.append($headSpan);
          }
          if(data.fixedRight) {
              $headSpan = $(dataHeadSpan);
              $headSpan.addClass('fixed-right')
                  // .find('.datatable-wrapper')
                  // .append('<div class="size-handle size-handle-head size-handle-right"></div>')
                  .find('table')
                  .addClass(options.tableClass)
                  .find('thead').append($right);
              $head.append($headSpan);
          }
          $datatable.append($head);
  
          // Rows
          var $rows = $('<div class="datatable-rows">');
          var $leftRow,
              $flexRow,
              $rightRow,
              // $tr,
              $td,
              $cTd,
              row,
              rowLen = rows.length,
              rowCol,
              rowColLen;
          $left = $('<tbody/>');
          $right = $('<tbody/>');
          $flex = $('<tbody/>');
  
          for(var r = 0; r < rowLen; ++r) {
              row = rows[r];
  
              // format row
              if(typeof row.id === 'undefined') {
                  row.id = r;
              }
              row.index = r;
  
              $leftRow = $('<tr class="datatable-row"/>');
              $leftRow.addClass(row.cssClass)
                  .toggleClass(options.checkedClass, !!row.checked)
                  .attr({
                      'data-index': r,
                      'data-id': row.id
                  });
              $flexRow = $leftRow.clone().addClass('datatable-row-flex');
              $rightRow = $leftRow.clone().addClass('datatable-row-right');
              $leftRow.addClass('datatable-row-left');
  
              rowColLen = row.data.length;
              for(i = 0; i < rowColLen; ++i) {
                  rowCol = row.data[i];
                  if(i > 0 && rowCol.empty) {
                      continue;
                  }
  
                  $tr = i < data.flexStart ? $leftRow : ((i >= data.flexStart && i <= data.flexEnd) ? $flexRow : $rightRow);
                  if(i === 0 && checkable) {
                      $cTd = $('<td data-index="check" class="check-row check-btn"><i class="icon-check-empty"></i></td>');
                      if(options.checkboxName) {
                          $cTd.append('<input class="hide" type="checkbox" name="' + options.checkboxName + '" value="' + row.id + '">');
                      }
                      $tr.append($cTd);
                  }
  
                  if(cols[i].ignore) continue;
  
                  // format row column
                  if(!$.isPlainObject(rowCol)) {
                      rowCol = {
                          text: rowCol,
                          row: r,
                          index: i
                      };
                  } else {
                      rowCol.row = r;
                      rowCol.index = i;
                  }
                  row.data[i] = rowCol;
  
                  $td = $('<td class="datatable-cell"/>');
  
                  $td.html(rowCol.text)
                      .addClass(rowCol.cssClass)
                      .addClass(cols[i].colClass)
                      .attr('colspan', rowCol.colSpan)
                      .attr({
                          'data-row': r,
                          'data-index': i,
                          'data-flex': false,
                          'data-type': cols[i].type,
                          style: rowCol.css,
                          title: rowCol.title || ''
                      }).css('width', cols[i].width);
  
  
                  $tr.append($td);
              }
  
              $left.append($leftRow);
              $flex.append($flexRow);
              $right.append($rightRow);
          }
  
  
          var $rowSpan;
          if(data.fixedLeft) {
              $rowSpan = $(dataRowSpan);
              $rowSpan.addClass('fixed-left')
                  .find('table')
                  .addClass(options.tableClass)
                  .append($left);
              $rows.append($rowSpan);
          }
          if(data.flexArea) {
              $rowSpan = $(dataRowSpan);
              $rowSpan.addClass('flexarea')
                  .find('.datatable-wrapper')
                  .append('<div class="scrolled-shadow scrolled-in-shadow"></div><div class="scrolled-shadow scrolled-out-shadow"></div>')
                  .find('table')
                  .addClass(options.tableClass)
                  .append($flex);
              $rows.append($rowSpan);
          }
          if(data.fixedRight) {
              $rowSpan = $(dataRowSpan);
              $rowSpan.addClass('fixed-right')
                  .find('table')
                  .addClass(options.tableClass)
                  .append($right);
              $rows.append($rowSpan);
          }
          $datatable.append($rows);
  
          if(data.flexArea) {
              $datatable.append('<div class="scroll-wrapper"><div class="scroll-slide scroll-pos-' + options.scrollPos + '"><div class="bar"></div></div></div>');
          }
  
          var $oldFooter = $datatable.children('.datatable-footer').detach();
          if(data.footer) {
              $datatable.append($('<div class="datatable-footer"/>').append(data.footer));
              data.footer = null;
          } else if($oldFooter.length) {
              $datatable.append($oldFooter);
          }
  
          that.$datatable = $datatable.data(name, that);
          if(that.isTable && that.firstShow) {
              that.$table.attr('data-datatable-id', this.id).hide().after($datatable);
              that.firstShow = false;
          }
  
          that.bindEvents();
          that.refreshSize();
          that.callEvent('render');
      };
  
      // Bind global events
      DataTable.prototype.bindEvents = function() {
          var that = this,
              data = this.data,
              options = this.options,
              $datatable = this.$datatable;
  
          var $dataSpans = that.$dataSpans = $datatable.children('.datatable-head, .datatable-rows').find('.datatable-span');
          var $rowsSpans = that.$rowsSpans = $datatable.children('.datatable-rows').children('.datatable-rows-span');
          var $headSpans = that.$headSpans = $datatable.children('.datatable-head').children('.datatable-head-span');
          var $cells = that.$cells = $dataSpans.find('.datatable-head-cell,.datatable-cell');
          var $dataCells = that.$dataCells = $cells.filter('.datatable-cell');
          that.$headCells = $cells.filter('.datatable-head-cell');
          var $rows = that.$rows = that.$rowsSpans.find('.datatable-row');
  
          // handle row hover events
          if(options.rowHover) {
              var hoverClass = options.hoverClass;
              $rowsSpans.on('mouseenter', '.datatable-cell', function() {
                  $dataCells.filter('.' + hoverClass).removeClass(hoverClass);
                  $rows.filter('.' + hoverClass).removeClass(hoverClass);
  
                  $rows.filter('[data-index="' + $(this).addClass(hoverClass).data('row') + '"]').addClass(hoverClass);
              }).on('mouseleave', '.datatable-cell', function() {
                  $dataCells.filter('.' + hoverClass).removeClass(hoverClass);
                  $rows.filter('.' + hoverClass).removeClass(hoverClass);
              });
          }
  
          // handle col hover events
          if(options.colHover) {
              var colHoverClass = options.colHoverClass;
              $headSpans.on('mouseenter', '.datatable-head-cell', function() {
                  $cells.filter('.' + colHoverClass).removeClass(colHoverClass);
                  $cells.filter('[data-index="' + $(this).data('index') + '"]').addClass(colHoverClass);
              }).on('mouseleave', '.datatable-head-cell', function() {
                  $cells.filter('.' + colHoverClass).removeClass(colHoverClass);
              });
          }
  
          // handle srcoll for flex area
          if(data.flexArea) {
              var $scrollbar = $datatable.find('.scroll-slide'),
                  $flexArea = $datatable.find('.datatable-span.flexarea'),
                  $fixedLeft = $datatable.find('.datatable-span.fixed-left'),
                  $flexTable = $datatable.find('.datatable-span.flexarea .table-datatable');
              var $bar = $scrollbar.children('.bar'),
                  flexWidth,
                  scrollWidth,
                  tableWidth,
                  barLeft,
                  scrollOffsetStoreName = that.id + '_' + 'scrollOffset',
                  firtScroll,
                  left;
  
              that.width = $datatable.width();
              $datatable.resize(function() {
                  that.width = $datatable.width();
              });
  
              var srollTable = function(offset, silence) {
                  barLeft = Math.max(0, Math.min(flexWidth - scrollWidth, offset));
                  if(!silence) {
                      $datatable.addClass('scrolling');
                  }
                  $bar.css('left', barLeft);
                  left = 0 - Math.floor((tableWidth - flexWidth) * barLeft / (flexWidth - scrollWidth));
                  $flexTable.css('left', left);
  
                  $datatable.toggleClass('scrolled-in', barLeft > 2)
                      .toggleClass('scrolled-out', barLeft < flexWidth - scrollWidth - 2);
  
                  if(options.storage) store.pageSet(scrollOffsetStoreName, barLeft);
              };
              var resizeScrollbar = function() {
                  flexWidth = $flexArea.width() - 2;
                  $scrollbar.width(flexWidth).css('left', $fixedLeft.width());
                  tableWidth = 0;
                  $flexTable.first().find('tr:first').children('td,th').each(function() {
                      tableWidth += $(this).outerWidth();
                  });
                  scrollWidth = Math.floor((flexWidth * flexWidth) / tableWidth);
                  $bar.css('width', scrollWidth);
                  $flexTable.css('min-width', Math.max(flexWidth, tableWidth));
                  $datatable.toggleClass('show-scroll-slide', tableWidth > flexWidth);
  
                  if(!firtScroll && flexWidth !== scrollWidth) {
                      firtScroll = true;
                      srollTable(store.pageGet(scrollOffsetStoreName, 0), true); // todo: unused?
                  }
  
                  if($datatable.hasClass('size-changing')) {
                      srollTable(barLeft, true);
                  }
              };
              $flexArea.resize(resizeScrollbar);
              if(options.storage) resizeScrollbar();
  
              var dragOptions = {
                  move: false,
                  stopPropagation: true,
                  drag: function(e) {
                      srollTable($bar.position().left + e.smallOffset.x * (e.element.hasClass('bar') ? 1 : -1));
                  },
                  finish: function() {
                      $datatable.removeClass('scrolling');
                  }
              };
  
              if($.fn.draggable) {
                  $bar.draggable(dragOptions);
                  if(options.flexHeadDrag) {
                      $datatable.find('.datatable-head-span.flexarea').draggable(dragOptions);
                  }
              } else {
                  console.error('DataTable requires draggable.js to improve UI.');
              }
  
              $scrollbar.mousedown(function(event) {
                  var x = event.pageX - $scrollbar.offset().left;
                  srollTable(x - (scrollWidth / 2));
              });
          }
  
          //  handle row check events
          if(options.checkable) {
              var checkedStatusStoreName = that.id + '_checkedStatus',
                  checkedClass = options.checkedClass,
                  rowId;
              var syncChecks = function() {
                  var $checkRows = $rowsSpans.first().find('.datatable-row');
                  var $checkedRows = $checkRows.filter('.' + checkedClass);
                  if(options.checkboxName) $checkRows.find('.check-row input:checkbox').prop('checked', false);
                  var checkedStatus = {
                      checkedAll: $checkRows.length === $checkedRows.length && $checkedRows.length > 0,
                      checks: $checkedRows.map(function() {
                          rowId = $(this).data('id');
                          if(options.checkboxName) {
                              $(this).find('.check-row input:checkbox').prop('checked', true);
                          }
                          return rowId;
                      }).toArray()
                  };
                  that.checks = checkedStatus;
                  $.each(data.rows, function(index, value) {
                      value.checked = ($.inArray(value.id, checkedStatus.checks) > -1);
                  });
                  $headSpans.find('.check-all').toggleClass('checked', !!checkedStatus.checkedAll);
  
                  if(options.storage) store.pageSet(checkedStatusStoreName, checkedStatus);
  
                  that.callEvent('checksChanged', {
                      checks: checkedStatus
                  });
              };
  
              var toggleRowClass = function(ele, toggle) {
                  var $tr = $(ele).closest('tr');
                  if(toggle === undefined) toggle = !$tr.hasClass(checkedClass);
                  $rows.filter('[data-index="' + $tr.data('index') + '"]').toggleClass(checkedClass, !!toggle);
              };
  
              var checkEventPrefix = 'click.zui.datatable.check';
              if(options.selectable) {
                  var selectableOptions = {
                      selector: '.datatable-rows .datatable-row',
                      trigger: '.datatable-rows',
                      start: function(e) {
                          var $checkRow = $(e.target).closest('.check-row, .check-btn');
                          if($checkRow.length) {
                              if($checkRow.is('.check-row')) {
                                  toggleRowClass($checkRow);
                                  syncChecks();
                              }
                              return false;
                          }
                      },
                      rangeFunc: function(range, targetRange) {
                          return Math.max(range.top, targetRange.top) < Math.min(range.top + range.height, targetRange.top + targetRange.height);
                      },
                      select: function(e) {
                          toggleRowClass(e.target, true);
                      },
                      unselect: function(e) {
                          toggleRowClass(e.target, false);
                      },
                      finish: function(e) {
                          syncChecks();
                      }
                  };
                  if($.isPlainObject(options.selectable)) {
                      $.extend(selectableOptions, options.selectable);
                  }
                  this.$datatable.selectable(selectableOptions);
              } else {
                  this.$rowsSpans.off(checkEventPrefix).on(checkEventPrefix + 'row', options.checkByClickRow ? 'tr' : '.check-row', function() {
                      toggleRowClass(this);
                      syncChecks();
                  });
              }
  
              this.$datatable.off(checkEventPrefix).on('click.zui.datatable.check', '.check-all', function() {
                  $rows.toggleClass(checkedClass, $(this).toggleClass('checked').hasClass('checked'));
                  syncChecks();
              }).on(checkEventPrefix + '.none', '.check-none', function() {
                  $rows.toggleClass(checkedClass, false);
                  syncChecks();
              }).on(checkEventPrefix + '.inverse', '.check-inverse', function() {
                  $rows.toggleClass(checkedClass);
                  syncChecks();
              });
  
              if(options.storage) {
                  var checkedStatus = store.pageGet(checkedStatusStoreName);
                  if(checkedStatus) {
                      $headSpans.find('.check-all').toggleClass('checked', checkedStatus.checkedAll);
                      if(checkedStatus.checkedAll) {
                          $rows.addClass(checkedClass);
                      } else {
                          $rows.removeClass(checkedClass);
                          $.each(checkedStatus.checks, function(index, ele) {
                              $rows.filter('[data-id="' + ele + '"]').addClass(checkedClass);
                          });
                      }
                      if(checkedStatus.checks.length) {
                          syncChecks();
                      }
                  }
              }
          }
  
          // fixed header
          if(options.fixedHeader) {
              var offsetTop,
                  height,
                  scrollTop,
                  $dataTableHead = $datatable.children('.datatable-head'),
                  navbarHeight = options.fixedHeaderOffset || $('.navbar.navbar-fixed-top').height() || 0;
              var handleScroll = function() {
                  offsetTop = $datatable.offset().top;
                  scrollTop = $(window).scrollTop();
                  height = $datatable.height();
                  $datatable.toggleClass('head-fixed', (scrollTop + navbarHeight) > offsetTop && (scrollTop + navbarHeight) < (offsetTop + height));
                  if($datatable.hasClass('head-fixed')) {
                      $dataTableHead.css({
                          width: $datatable.width(),
                          top: navbarHeight
                      });
                  } else {
                      $dataTableHead.attr('style', '');
                  }
              };
  
              $(window).scroll(handleScroll);
              handleScroll();
          }
  
          // handle sort
          if(options.sortable) {
              $headSpans.on('click', 'th:not(.sort-disabled, .check-btn)', function() {
                  if($datatable.hasClass('size-changing')) return;
                  that.sortTable($(this));
              });
  
              if(options.storage) that.sortTable();
          } else if(options.mergeRows) {
              this.mergeRows();
          }
      };
  
      DataTable.prototype.mergeRows = function() {
          var $cells = this.$rowsSpans.find('.datatable-cell');
          var cols = this.data.cols;
          for(var i = 0; i < cols.length; i++) {
              var col = cols[i];
              if(col.mergeRows) {
                  var $cs = $cells.filter('[data-index="' + i + '"]');
                  if($cs.length > 1) {
                      var $lastCell, rowspan;
                      $cs.each(function() {
                          var $cell = $(this);
                          if($lastCell) {
                              if($cell.html() === $lastCell.html()) {
                                  rowspan = $lastCell.attr('rowspan') || 1;
                                  if(typeof rowspan !== 'number') {
                                      rowspan = parseInt(rowspan);
                                      if(isNaN(rowspan)) rowspan = 1;
                                  }
  
                                  $lastCell.attr('rowspan', rowspan + 1).css('vertical-align', 'middle');
                                  $cell.remove();
                              } else {
                                  $lastCell = $cell;
                              }
                          } else {
                              $lastCell = $cell;
                          }
                      });
                  }
              }
          }
      };
  
      // Sort table
      DataTable.prototype.sortTable = function($th) {
          var store = $.zui.store,
              options = this.options;
          var sorterStoreName = this.id + '_datatableSorter';
          var sorter = options.storage ? store.pageGet(sorterStoreName) : null;
  
          // sort-down: desc
          // sort-up: asc
  
          if(!$th) {
              if(sorter) {
                  $th = this.$headCells.filter('[data-index="' + sorter.index + '"]').addClass('sort-' + (sorter.type === 'up' ? 'down' : 'up'));
              } else {
                  $th = this.$headCells.filter('.sort-up, .sort-down').first();
              }
          }
  
          if(!$th.length) {
              return;
          }
  
          var data = this.data;
          var cols = data.cols,
              rows = data.rows,
              $headCells = this.$headCells,
              sortUp,
              type,
              index;
  
          sortUp = !$th.hasClass('sort-up');
          if(data.keepSort) sortUp = !sortUp;
          data.keepSort = null;
  
          $headCells.removeClass('sort-up sort-down');
          $th.addClass(sortUp ? 'sort-up' : 'sort-down');
  
          index = $th.data('index');
  
          $.each(cols, function(idx, col) {
              if(idx != index && (col.sort === 'up' || col.sort === 'down')) {
                  col.sort = true;
              } else if(idx == index) {
                  col.sort = sortUp ? 'up' : 'down';
                  type = col.type;
              }
          });
  
          var valA, valB, result, $dataRows = this.$dataCells.filter('[data-index="' + index + '"]');
          rows.sort(function(cellA, cellB) {
              cellA = cellA.data[index];
              cellB = cellB.data[index];
              valA = $dataRows.filter('[data-row="' + cellA.row + '"]').text();
              valB = $dataRows.filter('[data-row="' + cellB.row + '"]').text();
              if(type === 'number') {
                  valA = parseFloat(valA);
                  valB = parseFloat(valB);
              } else if(type === 'date') {
                  valA = Date.parse(valA);
                  valB = Date.parse(valB);
              } else {
                  valA = valA.toLowerCase();
                  valB = valB.toLowerCase();
              }
              result = valA < valB ? 1 : (valA > valB ? -1 : 0);
              if(sortUp) {
                  result = result * (-1);
              }
              return result;
          });
  
          var $rows = this.$rows,
              lastRows = [],
              $row, $lastRow, $r;
          $.each(rows, function(idx, row) {
              $row = $rows.filter('[data-index="' + row.index + '"]');
              $row.each(function(rIdx) {
                  $r = $(this);
                  $lastRow = lastRows[rIdx];
                  if($lastRow) {
                      $lastRow.after($r);
                  } else {
                      $r.parent().prepend($r);
                  }
                  lastRows[rIdx] = $r;
              });
          });
  
          sorter = {
              index: index,
              type: sortUp ? 'up' : 'down'
          };
  
          // save sort with local storage
          if(options.storage) store.pageSet(sorterStoreName, sorter);
  
          this.callEvent('sort', {
              sorter: sorter
          });
      };
  
      // Refresh size
      DataTable.prototype.refreshSize = function() {
          var $datatable = this.$datatable,
              options = this.options,
              rows = this.data.rows,
              cols = this.data.cols,
              i;
  
          $datatable.find('.datatable-span.fixed-left').css('width', options.fixedLeftWidth);
          $datatable.find('.datatable-span.fixed-right').css('width', options.fixedRightWidth);
  
          if(options.fixCellHeight) {
              var findMaxHeight = function($cells) {
                  var mx = 0,
                      $cell, rowSpan;
                  $cells.css('height', 'auto');
                  $cells.each(function() {
                      $cell = $(this);
                      rowSpan = $cell.attr('rowspan');
                      if(!rowSpan || rowSpan == 1) mx = Math.max(mx, $cell.outerHeight());
                  });
                  return mx;
              };
              var $dataCells = this.$dataCells,
                  $cells = this.$cells,
                  $headCells = this.$headCells;
  
              // set height of head cells
              var headMaxHeight = findMaxHeight($headCells);
              $headCells.css('min-height', headMaxHeight).css('height', headMaxHeight);
  
              // set height of data cells
              var $rowCells;
              for(i = 0; i < rows.length; ++i) {
                  $rowCells = $dataCells.filter('[data-row="' + i + '"]');
                  var rowMaxHeight = findMaxHeight($rowCells);
                  $rowCells.css('min-height', rowMaxHeight).css('height', rowMaxHeight);
              }
          }
      };
  
      // Call event
      DataTable.prototype.callEvent = function(name, params) {
          var result = this.$.callEvent(name + '.' + this.name, params, this).result;
          return !(result !== undefined && (!result));
      };
  
      $.fn.datatable = function(option, newData) {
          return this.each(function() {
              var $this = $(this);
              var data = $this.data(name);
              var options = typeof option == 'object' && option;
  
              if(!data) $this.data(name, (data = new DataTable(this, options)));
  
              if(typeof option == 'string') {
                  if(option === 'load' && $.isPlainObject(newData) && (newData.keepSort === undefined || newData.keepSort === null)) newData.keepSort = true;
                  data[option](newData);
              }
          });
      };
  
      $.fn.datatable.Constructor = DataTable;
  }(jQuery));
  