/*
 * jsGrid v1.5.3 (http://js-grid.com)
 * (c) 2016 Artem Tabalin
 * Licensed under MIT (https://github.com/tabalinas/jsgrid/blob/master/LICENSE)
 */

(function (window, $, undefined) {
  var JSGRID = "JSGrid",
    JSGRID_DATA_KEY = JSGRID,
    JSGRID_ROW_DATA_KEY = "JSGridItem",
    JSGRID_EDIT_ROW_DATA_KEY = "JSGridEditRow",
    SORT_ORDER_ASC = "asc",
    SORT_ORDER_DESC = "desc",
    FIRST_PAGE_PLACEHOLDER = "{first}",
    PAGES_PLACEHOLDER = "{pages}",
    PREV_PAGE_PLACEHOLDER = "{prev}",
    NEXT_PAGE_PLACEHOLDER = "{next}",
    LAST_PAGE_PLACEHOLDER = "{last}",
    PAGE_INDEX_PLACEHOLDER = "{pageIndex}",
    PAGE_COUNT_PLACEHOLDER = "{pageCount}",
    ITEM_COUNT_PLACEHOLDER = "{itemCount}",
    EMPTY_HREF = "javascript:void(0);";

  var getOrApply = function (value, context) {
    if ($.isFunction(value)) {
      return value.apply(context, $.makeArray(arguments).slice(2));
    }
    return value;
  };

  var normalizePromise = function (promise) {
    var d = $.Deferred();

    if (promise && promise.then) {
      promise.then(
        function () {
          d.resolve.apply(d, arguments);
        },
        function () {
          d.reject.apply(d, arguments);
        }
      );
    } else {
      d.resolve(promise);
    }

    return d.promise();
  };

  var defaultController = {
    loadData: $.noop,
    insertItem: $.noop,
    updateItem: $.noop,
    deleteItem: $.noop,
  };

  function Grid(element, config) {
    var $element = $(element);

    $element.data(JSGRID_DATA_KEY, this);

    this._container = $element;

    this.data = [];
    this.fields = [];

    this._editingRow = null;
    this._sortField = null;
    this._sortOrder = SORT_ORDER_ASC;
    this._firstDisplayingPage = 1;

    this._init(config);
    this.render();
  }

  Grid.prototype = {
    width: "auto",
    height: "auto",
    updateOnResize: true,

    rowClass: $.noop,
    rowRenderer: null,

    rowClick: function (args) {
      if (this.editing) {
        this.editItem($(args.event.target).closest("tr"));
      }
    },
    rowDoubleClick: $.noop,

    noDataContent: "Not found",
    noDataRowClass: "jsgrid-nodata-row",

    heading: true,
    headerRowRenderer: null,
    headerRowClass: "jsgrid-header-row",
    headerCellClass: "jsgrid-header-cell",

    filtering: false,
    filterRowRenderer: null,
    filterRowClass: "jsgrid-filter-row",

    inserting: false,
    insertRowRenderer: null,
    insertRowClass: "jsgrid-insert-row",

    editing: false,
    editRowRenderer: null,
    editRowClass: "jsgrid-edit-row",

    confirmDeleting: true,
    deleteConfirm: "Are you sure?",

    selecting: true,
    selectedRowClass: "jsgrid-selected-row",
    oddRowClass: "jsgrid-row",
    evenRowClass: "jsgrid-alt-row",
    cellClass: "jsgrid-cell",

    sorting: false,
    sortableClass: "jsgrid-header-sortable",
    sortAscClass: "jsgrid-header-sort jsgrid-header-sort-asc",
    sortDescClass: "jsgrid-header-sort jsgrid-header-sort-desc",

    paging: false,
    pagerContainer: null,
    pageIndex: 1,
    pageSize: 20,
    pageButtonCount: 15,
    pagerFormat:
      "Pages: {first} {prev} {pages} {next} {last} &nbsp;&nbsp; {pageIndex} of {pageCount}",
    pagePrevText: "Prev",
    pageNextText: "Next",
    pageFirstText: "First",
    pageLastText: "Last",
    pageNavigatorNextText: "...",
    pageNavigatorPrevText: "...",
    pagerContainerClass: "jsgrid-pager-container",
    pagerClass: "jsgrid-pager",
    pagerNavButtonClass: "jsgrid-pager-nav-button",
    pagerNavButtonInactiveClass: "jsgrid-pager-nav-inactive-button",
    pageClass: "jsgrid-pager-page",
    currentPageClass: "jsgrid-pager-current-page",

    customLoading: false,
    pageLoading: false,

    autoload: false,
    controller: defaultController,

    loadIndication: true,
    loadIndicationDelay: 500,
    loadMessage: "Please, wait...",
    loadShading: true,

    invalidMessage: "Invalid data entered!",

    invalidNotify: function (args) {
      var messages = $.map(args.errors, function (error) {
        return error.message || null;
      });

      window.alert([this.invalidMessage].concat(messages).join("\n"));
    },

    onInit: $.noop,
    onRefreshing: $.noop,
    onRefreshed: $.noop,
    onPageChanged: $.noop,
    onItemDeleting: $.noop,
    onItemDeleted: $.noop,
    onItemInserting: $.noop,
    onItemInserted: $.noop,
    onItemEditing: $.noop,
    onItemUpdating: $.noop,
    onItemUpdated: $.noop,
    onItemInvalid: $.noop,
    onDataLoading: $.noop,
    onDataLoaded: $.noop,
    onOptionChanging: $.noop,
    onOptionChanged: $.noop,
    onError: $.noop,

    invalidClass: "jsgrid-invalid",

    containerClass: "jsgrid",
    tableClass: "jsgrid-table",
    gridHeaderClass: "jsgrid-grid-header",
    gridBodyClass: "jsgrid-grid-body",

    _init: function (config) {
      $.extend(this, config);
      this._initLoadStrategy();
      this._initController();
      this._initFields();
      this._attachWindowLoadResize();
      this._attachWindowResizeCallback();
      this._callEventHandler(this.onInit);
    },

    loadStrategy: function () {
      return this.pageLoading
        ? new jsGrid.loadStrategies.PageLoadingStrategy(this)
        : new jsGrid.loadStrategies.DirectLoadingStrategy(this);
    },

    _initLoadStrategy: function () {
      this._loadStrategy = getOrApply(this.loadStrategy, this);
    },

    _initController: function () {
      this._controller = $.extend(
        {},
        defaultController,
        getOrApply(this.controller, this)
      );
    },

    renderTemplate: function (source, context, config) {
      args = [];
      for (var key in config) {
        args.push(config[key]);
      }

      args.unshift(source, context);

      source = getOrApply.apply(null, args);
      return source === undefined || source === null ? "" : source;
    },

    loadIndicator: function (config) {
      return new jsGrid.LoadIndicator(config);
    },

    validation: function (config) {
      return jsGrid.Validation && new jsGrid.Validation(config);
    },

    _initFields: function () {
      var self = this;
      self.fields = $.map(self.fields, function (field) {
        if ($.isPlainObject(field)) {
          var fieldConstructor =
            (field.type && jsGrid.fields[field.type]) || jsGrid.Field;
          field = new fieldConstructor(field);
        }
        field._grid = self;
        return field;
      });
    },

    _attachWindowLoadResize: function () {
      $(window).on("load", $.proxy(this._refreshSize, this));
    },

    _attachWindowResizeCallback: function () {
      if (this.updateOnResize) {
        $(window).on("resize", $.proxy(this._refreshSize, this));
      }
    },

    _detachWindowResizeCallback: function () {
      $(window).off("resize", this._refreshSize);
    },

    option: function (key, value) {
      var optionChangingEventArgs, optionChangedEventArgs;

      if (arguments.length === 1) return this[key];

      optionChangingEventArgs = {
        option: key,
        oldValue: this[key],
        newValue: value,
      };
      this._callEventHandler(this.onOptionChanging, optionChangingEventArgs);

      this._handleOptionChange(
        optionChangingEventArgs.option,
        optionChangingEventArgs.newValue
      );

      optionChangedEventArgs = {
        option: optionChangingEventArgs.option,
        value: optionChangingEventArgs.newValue,
      };
      this._callEventHandler(this.onOptionChanged, optionChangedEventArgs);
    },

    fieldOption: function (field, key, value) {
      field = this._normalizeField(field);

      if (arguments.length === 2) return field[key];

      field[key] = value;
      this._renderGrid();
    },

    _handleOptionChange: function (name, value) {
      this[name] = value;

      switch (name) {
        case "width":
        case "height":
          this._refreshSize();
          break;
        case "rowClass":
        case "rowRenderer":
        case "rowClick":
        case "rowDoubleClick":
        case "noDataRowClass":
        case "noDataContent":
        case "selecting":
        case "selectedRowClass":
        case "oddRowClass":
        case "evenRowClass":
          this._refreshContent();
          break;
        case "pageButtonCount":
        case "pagerFormat":
        case "pagePrevText":
        case "pageNextText":
        case "pageFirstText":
        case "pageLastText":
        case "pageNavigatorNextText":
        case "pageNavigatorPrevText":
        case "pagerClass":
        case "pagerNavButtonClass":
        case "pageClass":
        case "currentPageClass":
        case "pagerRenderer":
          this._refreshPager();
          break;
        case "fields":
          this._initFields();
          this.render();
          break;
        case "data":
        case "editing":
        case "heading":
        case "filtering":
        case "inserting":
        case "paging":
          this.refresh();
          break;
        case "loadStrategy":
        case "pageLoading":
          this._initLoadStrategy();
          this.search();
          break;
        case "pageIndex":
          this.openPage(value);
          break;
        case "pageSize":
          this.refresh();
          this.search();
          break;
        case "editRowRenderer":
        case "editRowClass":
          this.cancelEdit();
          break;
        case "updateOnResize":
          this._detachWindowResizeCallback();
          this._attachWindowResizeCallback();
          break;
        case "invalidNotify":
        case "invalidMessage":
          break;
        default:
          this.render();
          break;
      }
    },

    destroy: function () {
      this._detachWindowResizeCallback();
      this._clear();
      this._container.removeData(JSGRID_DATA_KEY);
    },

    render: function () {
      this._renderGrid();
      return this.autoload ? this.loadData() : $.Deferred().resolve().promise();
    },

    _renderGrid: function () {
      this._clear();

      this._container
        .addClass(this.containerClass)
        .css("position", "relative")
        .append(this._createHeader())
        .append(this._createBody());

      this._pagerContainer = this._createPagerContainer();
      this._loadIndicator = this._createLoadIndicator();
      this._validation = this._createValidation();

      this.refresh();
    },

    _createLoadIndicator: function () {
      return getOrApply(this.loadIndicator, this, {
        message: this.loadMessage,
        shading: this.loadShading,
        container: this._container,
      });
    },

    _createValidation: function () {
      return getOrApply(this.validation, this);
    },

    _clear: function () {
      this.cancelEdit();

      clearTimeout(this._loadingTimer);

      this._pagerContainer && this._pagerContainer.empty();

      this._container.empty().css({ position: "", width: "", height: "" });
    },

    _createHeader: function () {
      var $headerRow = (this._headerRow = this._createHeaderRow()),
        $filterRow = (this._filterRow = this._createFilterRow()),
        $insertRow = (this._insertRow = this._createInsertRow());

      var $headerGrid = (this._headerGrid = $("<table>")
        .addClass(this.tableClass)
        .append($headerRow)
        .append($filterRow)
        .append($insertRow));

      var $header = (this._header = $("<div>")
        .addClass(this.gridHeaderClass)
        .addClass(this._scrollBarWidth() ? "jsgrid-header-scrollbar" : "")
        .append($headerGrid));

      return $header;
    },

    _createBody: function () {
      var $content = (this._content = $("<tbody>"));

      var $bodyGrid = (this._bodyGrid = $("<table>")
        .addClass(this.tableClass)
        .append($content));

      var $body = (this._body = $("<div>")
        .addClass(this.gridBodyClass)
        .append($bodyGrid)
        .on(
          "scroll",
          $.proxy(function (e) {
            this._header.scrollLeft(e.target.scrollLeft);
          }, this)
        ));

      return $body;
    },

    _createPagerContainer: function () {
      var pagerContainer =
        this.pagerContainer || $("<div>").appendTo(this._container);
      return $(pagerContainer).addClass(this.pagerContainerClass);
    },

    _eachField: function (callBack) {
      var self = this;
      $.each(this.fields, function (index, field) {
        if (field.visible) {
          callBack.call(self, field, index);
        }
      });
    },

    _createHeaderRow: function () {
      if ($.isFunction(this.headerRowRenderer))
        return $(this.renderTemplate(this.headerRowRenderer, this));

      var $result = $("<tr>").addClass(this.headerRowClass);

      this._eachField(function (field, index) {
        var $th = this._prepareCell(
          "<th>",
          field,
          "headercss",
          this.headerCellClass
        )
          .append(this.renderTemplate(field.headerTemplate, field))
          .appendTo($result);

        if (this.sorting && field.sorting) {
          $th.addClass(this.sortableClass).on(
            "click",
            $.proxy(function () {
              this.sort(index);
            }, this)
          );
        }
      });

      return $result;
    },

    _prepareCell: function (cell, field, cssprop, cellClass) {
      return $(cell)
        .css("width", field.width)
        .addClass(cellClass || this.cellClass)
        .addClass((cssprop && field[cssprop]) || field.css)
        .addClass(field.align ? "jsgrid-align-" + field.align : "");
    },

    _createFilterRow: function () {
      if ($.isFunction(this.filterRowRenderer))
        return $(this.renderTemplate(this.filterRowRenderer, this));

      var $result = $("<tr>").addClass(this.filterRowClass);

      this._eachField(function (field) {
        this._prepareCell("<td>", field, "filtercss")
          .append(this.renderTemplate(field.filterTemplate, field))
          .appendTo($result);
      });

      return $result;
    },

    _createInsertRow: function () {
      if ($.isFunction(this.insertRowRenderer))
        return $(this.renderTemplate(this.insertRowRenderer, this));

      var $result = $("<tr>").addClass(this.insertRowClass);

      this._eachField(function (field) {
        this._prepareCell("<td>", field, "insertcss")
          .append(this.renderTemplate(field.insertTemplate, field))
          .appendTo($result);
      });

      return $result;
    },

    _callEventHandler: function (handler, eventParams) {
      handler.call(
        this,
        $.extend(eventParams, {
          grid: this,
        })
      );

      return eventParams;
    },

    reset: function () {
      this._resetSorting();
      this._resetPager();
      return this._loadStrategy.reset();
    },

    _resetPager: function () {
      this._firstDisplayingPage = 1;
      this._setPage(1);
    },

    _resetSorting: function () {
      this._sortField = null;
      this._sortOrder = SORT_ORDER_ASC;
      this._clearSortingCss();
    },

    refresh: function () {
      this._callEventHandler(this.onRefreshing);

      this.cancelEdit();

      this._refreshHeading();
      this._refreshFiltering();
      this._refreshInserting();
      this._refreshContent();
      this._refreshPager();
      this._refreshSize();

      this._callEventHandler(this.onRefreshed);
    },

    _refreshHeading: function () {
      this._headerRow.toggle(this.heading);
    },

    _refreshFiltering: function () {
      this._filterRow.toggle(this.filtering);
    },

    _refreshInserting: function () {
      this._insertRow.toggle(this.inserting);
    },

    _refreshContent: function () {
      var $content = this._content;
      $content.empty();

      if (!this.data.length) {
        $content.append(this._createNoDataRow());
        return this;
      }

      var indexFrom = this._loadStrategy.firstDisplayIndex();
      var indexTo = this._loadStrategy.lastDisplayIndex();

      for (var itemIndex = indexFrom; itemIndex < indexTo; itemIndex++) {
        var item = this.data[itemIndex];
        $content.append(this._createRow(item, itemIndex));
      }
    },

    _createNoDataRow: function () {
      var amountOfFields = 0;
      this._eachField(function () {
        amountOfFields++;
      });

      return $("<tr>")
        .addClass(this.noDataRowClass)
        .append(
          $("<td>")
            .addClass(this.cellClass)
            .attr("colspan", amountOfFields)
            .append(this.renderTemplate(this.noDataContent, this))
        );
    },

    _createRow: function (item, itemIndex) {
      var $result;

      if ($.isFunction(this.rowRenderer)) {
        $result = this.renderTemplate(this.rowRenderer, this, {
          item: item,
          itemIndex: itemIndex,
        });
      } else {
        $result = $("<tr>");
        this._renderCells($result, item);
      }

      $result
        .addClass(this._getRowClasses(item, itemIndex))
        .data(JSGRID_ROW_DATA_KEY, item)
        .on(
          "click",
          $.proxy(function (e) {
            this.rowClick({
              item: item,
              itemIndex: itemIndex,
              event: e,
            });
          }, this)
        )
        .on(
          "dblclick",
          $.proxy(function (e) {
            this.rowDoubleClick({
              item: item,
              itemIndex: itemIndex,
              event: e,
            });
          }, this)
        );

      if (this.selecting) {
        this._attachRowHover($result);
      }

      return $result;
    },

    _getRowClasses: function (item, itemIndex) {
      var classes = [];
      classes.push((itemIndex + 1) % 2 ? this.oddRowClass : this.evenRowClass);
      classes.push(getOrApply(this.rowClass, this, item, itemIndex));
      return classes.join(" ");
    },

    _attachRowHover: function ($row) {
      var selectedRowClass = this.selectedRowClass;
      $row.hover(
        function () {
          $(this).addClass(selectedRowClass);
        },
        function () {
          $(this).removeClass(selectedRowClass);
        }
      );
    },

    _renderCells: function ($row, item) {
      this._eachField(function (field) {
        $row.append(this._createCell(item, field));
      });
      return this;
    },

    _createCell: function (item, field) {
      var $result;
      var fieldValue = this._getItemFieldValue(item, field);

      var args = { value: fieldValue, item: item };
      if ($.isFunction(field.cellRenderer)) {
        $result = this.renderTemplate(field.cellRenderer, field, args);
      } else {
        $result = $("<td>").append(
          this.renderTemplate(field.itemTemplate || fieldValue, field, args)
        );
      }

      return this._prepareCell($result, field);
    },

    _getItemFieldValue: function (item, field) {
      var props = field.name.split(".");
      var result = item[props.shift()];

      while (result && props.length) {
        result = result[props.shift()];
      }

      return result;
    },

    _setItemFieldValue: function (item, field, value) {
      var props = field.name.split(".");
      var current = item;
      var prop = props[0];

      while (current && props.length) {
        item = current;
        prop = props.shift();
        current = item[prop];
      }

      if (!current) {
        while (props.length) {
          item = item[prop] = {};
          prop = props.shift();
        }
      }

      item[prop] = value;
    },

    sort: function (field, order) {
      if ($.isPlainObject(field)) {
        order = field.order;
        field = field.field;
      }

      this._clearSortingCss();
      this._setSortingParams(field, order);
      this._setSortingCss();
      return this._loadStrategy.sort();
    },

    _clearSortingCss: function () {
      this._headerRow
        .find("th")
        .removeClass(this.sortAscClass)
        .removeClass(this.sortDescClass);
    },

    _setSortingParams: function (field, order) {
      field = this._normalizeField(field);
      order =
        order ||
        (this._sortField === field
          ? this._reversedSortOrder(this._sortOrder)
          : SORT_ORDER_ASC);

      this._sortField = field;
      this._sortOrder = order;
    },

    _normalizeField: function (field) {
      if ($.isNumeric(field)) {
        return this.fields[field];
      }

      if (typeof field === "string") {
        return $.grep(this.fields, function (f) {
          return f.name === field;
        })[0];
      }

      return field;
    },

    _reversedSortOrder: function (order) {
      return order === SORT_ORDER_ASC ? SORT_ORDER_DESC : SORT_ORDER_ASC;
    },

    _setSortingCss: function () {
      var fieldIndex = this._visibleFieldIndex(this._sortField);

      this._headerRow
        .find("th")
        .eq(fieldIndex)
        .addClass(
          this._sortOrder === SORT_ORDER_ASC
            ? this.sortAscClass
            : this.sortDescClass
        );
    },

    _visibleFieldIndex: function (field) {
      return $.inArray(
        field,
        $.grep(this.fields, function (f) {
          return f.visible;
        })
      );
    },

    _sortData: function () {
      var sortFactor = this._sortFactor(),
        sortField = this._sortField;

      if (sortField) {
        this.data.sort(function (item1, item2) {
          return (
            sortFactor *
            sortField.sortingFunc(item1[sortField.name], item2[sortField.name])
          );
        });
      }
    },

    _sortFactor: function () {
      return this._sortOrder === SORT_ORDER_ASC ? 1 : -1;
    },

    _itemsCount: function () {
      return this._loadStrategy.itemsCount();
    },

    _pagesCount: function () {
      var itemsCount = this._itemsCount(),
        pageSize = this.pageSize;
      return (
        Math.floor(itemsCount / pageSize) + (itemsCount % pageSize ? 1 : 0)
      );
    },

    _refreshPager: function () {
      var $pagerContainer = this._pagerContainer;
      $pagerContainer.empty();

      if (this.paging) {
        $pagerContainer.append(this._createPager());
      }

      var showPager = this.paging && this._pagesCount() > 1;
      $pagerContainer.toggle(showPager);
    },

    _createPager: function () {
      var $result;

      if ($.isFunction(this.pagerRenderer)) {
        $result = $(
          this.pagerRenderer({
            pageIndex: this.pageIndex,
            pageCount: this._pagesCount(),
          })
        );
      } else {
        $result = $("<div>").append(this._createPagerByFormat());
      }

      $result.addClass(this.pagerClass);

      return $result;
    },

    _createPagerByFormat: function () {
      var pageIndex = this.pageIndex,
        pageCount = this._pagesCount(),
        itemCount = this._itemsCount(),
        pagerParts = this.pagerFormat.split(" ");

      return $.map(
        pagerParts,
        $.proxy(function (pagerPart) {
          var result = pagerPart;

          if (pagerPart === PAGES_PLACEHOLDER) {
            result = this._createPages();
          } else if (pagerPart === FIRST_PAGE_PLACEHOLDER) {
            result = this._createPagerNavButton(
              this.pageFirstText,
              1,
              pageIndex > 1
            );
          } else if (pagerPart === PREV_PAGE_PLACEHOLDER) {
            result = this._createPagerNavButton(
              this.pagePrevText,
              pageIndex - 1,
              pageIndex > 1
            );
          } else if (pagerPart === NEXT_PAGE_PLACEHOLDER) {
            result = this._createPagerNavButton(
              this.pageNextText,
              pageIndex + 1,
              pageIndex < pageCount
            );
          } else if (pagerPart === LAST_PAGE_PLACEHOLDER) {
            result = this._createPagerNavButton(
              this.pageLastText,
              pageCount,
              pageIndex < pageCount
            );
          } else if (pagerPart === PAGE_INDEX_PLACEHOLDER) {
            result = pageIndex;
          } else if (pagerPart === PAGE_COUNT_PLACEHOLDER) {
            result = pageCount;
          } else if (pagerPart === ITEM_COUNT_PLACEHOLDER) {
            result = itemCount;
          }

          return $.isArray(result) ? result.concat([" "]) : [result, " "];
        }, this)
      );
    },

    _createPages: function () {
      var pageCount = this._pagesCount(),
        pageButtonCount = this.pageButtonCount,
        firstDisplayingPage = this._firstDisplayingPage,
        pages = [];

      if (firstDisplayingPage > 1) {
        pages.push(
          this._createPagerPageNavButton(
            this.pageNavigatorPrevText,
            this.showPrevPages
          )
        );
      }

      for (
        var i = 0, pageNumber = firstDisplayingPage;
        i < pageButtonCount && pageNumber <= pageCount;
        i++, pageNumber++
      ) {
        pages.push(
          pageNumber === this.pageIndex
            ? this._createPagerCurrentPage()
            : this._createPagerPage(pageNumber)
        );
      }

      if (firstDisplayingPage + pageButtonCount - 1 < pageCount) {
        pages.push(
          this._createPagerPageNavButton(
            this.pageNavigatorNextText,
            this.showNextPages
          )
        );
      }

      return pages;
    },

    _createPagerNavButton: function (text, pageIndex, isActive) {
      return this._createPagerButton(
        text,
        this.pagerNavButtonClass +
          (isActive ? "" : " " + this.pagerNavButtonInactiveClass),
        isActive
          ? function () {
              this.openPage(pageIndex);
            }
          : $.noop
      );
    },

    _createPagerPageNavButton: function (text, handler) {
      return this._createPagerButton(text, this.pagerNavButtonClass, handler);
    },

    _createPagerPage: function (pageIndex) {
      return this._createPagerButton(pageIndex, this.pageClass, function () {
        this.openPage(pageIndex);
      });
    },

    _createPagerButton: function (text, css, handler) {
      var $link = $("<a>")
        .attr("href", EMPTY_HREF)
        .html(text)
        .on("click", $.proxy(handler, this));

      return $("<span>").addClass(css).append($link);
    },

    _createPagerCurrentPage: function () {
      return $("<span>")
        .addClass(this.pageClass)
        .addClass(this.currentPageClass)
        .text(this.pageIndex);
    },

    _refreshSize: function () {
      this._refreshHeight();
      this._refreshWidth();
    },

    _refreshWidth: function () {
      var width = this.width === "auto" ? this._getAutoWidth() : this.width;

      this._container.width(width);
    },

    _getAutoWidth: function () {
      var $headerGrid = this._headerGrid,
        $header = this._header;

      $headerGrid.width("auto");

      var contentWidth = $headerGrid.outerWidth();
      var borderWidth = $header.outerWidth() - $header.innerWidth();

      $headerGrid.width("");

      return contentWidth + borderWidth;
    },

    _scrollBarWidth: (function () {
      var result;

      return function () {
        if (result === undefined) {
          var $ghostContainer = $(
            "<div style='width:50px;height:50px;overflow:hidden;position:absolute;top:-10000px;left:-10000px;'></div>"
          );
          var $ghostContent = $("<div style='height:100px;'></div>");
          $ghostContainer.append($ghostContent).appendTo("body");
          var width = $ghostContent.innerWidth();
          $ghostContainer.css("overflow-y", "auto");
          var widthExcludingScrollBar = $ghostContent.innerWidth();
          $ghostContainer.remove();
          result = width - widthExcludingScrollBar;
        }
        return result;
      };
    })(),

    _refreshHeight: function () {
      var container = this._container,
        pagerContainer = this._pagerContainer,
        height = this.height,
        nonBodyHeight;

      container.height(height);

      if (height !== "auto") {
        height = container.height();

        nonBodyHeight = this._header.outerHeight(true);
        if (pagerContainer.parents(container).length) {
          nonBodyHeight += pagerContainer.outerHeight(true);
        }

        this._body.outerHeight(height - nonBodyHeight);
      }
    },

    showPrevPages: function () {
      var firstDisplayingPage = this._firstDisplayingPage,
        pageButtonCount = this.pageButtonCount;

      this._firstDisplayingPage =
        firstDisplayingPage > pageButtonCount
          ? firstDisplayingPage - pageButtonCount
          : 1;

      this._refreshPager();
    },

    showNextPages: function () {
      var firstDisplayingPage = this._firstDisplayingPage,
        pageButtonCount = this.pageButtonCount,
        pageCount = this._pagesCount();

      this._firstDisplayingPage =
        firstDisplayingPage + 2 * pageButtonCount > pageCount
          ? pageCount - pageButtonCount + 1
          : firstDisplayingPage + pageButtonCount;

      this._refreshPager();
    },

    openPage: function (pageIndex) {
      if (pageIndex < 1 || pageIndex > this._pagesCount()) return;

      this._setPage(pageIndex);
      this._loadStrategy.openPage(pageIndex);
    },

    _setPage: function (pageIndex) {
      var firstDisplayingPage = this._firstDisplayingPage,
        pageButtonCount = this.pageButtonCount;

      this.pageIndex = pageIndex;

      if (pageIndex < firstDisplayingPage) {
        this._firstDisplayingPage = pageIndex;
      }

      if (pageIndex > firstDisplayingPage + pageButtonCount - 1) {
        this._firstDisplayingPage = pageIndex - pageButtonCount + 1;
      }

      this._callEventHandler(this.onPageChanged, {
        pageIndex: pageIndex,
      });
    },

    _controllerCall: function (method, param, isCanceled, doneCallback) {
      if (isCanceled) return $.Deferred().reject().promise();

      this._showLoading();

      var controller = this._controller;
      if (!controller || !controller[method]) {
        throw Error("controller has no method '" + method + "'");
      }

      return normalizePromise(controller[method](param))
        .done($.proxy(doneCallback, this))
        .fail($.proxy(this._errorHandler, this))
        .always($.proxy(this._hideLoading, this));
    },

    _errorHandler: function () {
      this._callEventHandler(this.onError, {
        args: $.makeArray(arguments),
      });
    },

    _showLoading: function () {
      if (!this.loadIndication) return;

      clearTimeout(this._loadingTimer);

      this._loadingTimer = setTimeout(
        $.proxy(function () {
          this._loadIndicator.show();
        }, this),
        this.loadIndicationDelay
      );
    },

    _hideLoading: function () {
      if (!this.loadIndication) return;

      clearTimeout(this._loadingTimer);
      this._loadIndicator.hide();
    },

    search: function (filter) {
      this._resetSorting();
      this._resetPager();
      return this.loadData(filter);
    },

    loadData: function (filter) {
      filter = filter || (this.filtering ? this.getFilter() : {});

      $.extend(filter, this._loadStrategy.loadParams(), this._sortingParams());

      var args = this._callEventHandler(this.onDataLoading, {
        filter: filter,
      });

      return this._controllerCall(
        "loadData",
        filter,
        args.cancel,
        function (loadedData) {
          if (!loadedData) return;

          this._loadStrategy.finishLoad(loadedData);

          this._callEventHandler(this.onDataLoaded, {
            data: loadedData,
          });
        }
      );
    },

    getFilter: function () {
      var result = {};
      this._eachField(function (field) {
        if (field.filtering) {
          this._setItemFieldValue(result, field, field.filterValue());
        }
      });
      return result;
    },

    _sortingParams: function () {
      if (this.sorting && this._sortField) {
        return {
          sortField: this._sortField.name,
          sortOrder: this._sortOrder,
        };
      }
      return {};
    },

    getSorting: function () {
      var sortingParams = this._sortingParams();
      return {
        field: sortingParams.sortField,
        order: sortingParams.sortOrder,
      };
    },

    clearFilter: function () {
      var $filterRow = this._createFilterRow();
      this._filterRow.replaceWith($filterRow);
      this._filterRow = $filterRow;
      return this.search();
    },

    insertItem: function (item) {
      var insertingItem = item || this._getValidatedInsertItem();

      if (!insertingItem) return $.Deferred().reject().promise();

      var args = this._callEventHandler(this.onItemInserting, {
        item: insertingItem,
      });

      return this._controllerCall(
        "insertItem",
        insertingItem,
        args.cancel,
        function (insertedItem) {
          insertedItem = insertedItem || insertingItem;
          this._loadStrategy.finishInsert(insertedItem);

          this._callEventHandler(this.onItemInserted, {
            item: insertedItem,
          });
        }
      );
    },

    _getValidatedInsertItem: function () {
      var item = this._getInsertItem();
      return this._validateItem(item, this._insertRow) ? item : null;
    },

    _getInsertItem: function () {
      var result = {};
      this._eachField(function (field) {
        if (field.inserting) {
          this._setItemFieldValue(result, field, field.insertValue());
        }
      });
      return result;
    },

    _validateItem: function (item, $row) {
      var validationErrors = [];

      var args = {
        item: item,
        itemIndex: this._rowIndex($row),
        row: $row,
      };

      this._eachField(function (field) {
        if (
          !field.validate ||
          ($row === this._insertRow && !field.inserting) ||
          ($row === this._getEditRow() && !field.editing)
        )
          return;

        var fieldValue = this._getItemFieldValue(item, field);

        var errors = this._validation.validate(
          $.extend(
            {
              value: fieldValue,
              rules: field.validate,
            },
            args
          )
        );

        this._setCellValidity(
          $row.children().eq(this._visibleFieldIndex(field)),
          errors
        );

        if (!errors.length) return;

        validationErrors.push.apply(
          validationErrors,
          $.map(errors, function (message) {
            return { field: field, message: message };
          })
        );
      });

      if (!validationErrors.length) return true;

      var invalidArgs = $.extend(
        {
          errors: validationErrors,
        },
        args
      );
      this._callEventHandler(this.onItemInvalid, invalidArgs);
      this.invalidNotify(invalidArgs);

      return false;
    },

    _setCellValidity: function ($cell, errors) {
      $cell
        .toggleClass(this.invalidClass, !!errors.length)
        .attr("title", errors.join("\n"));
    },

    clearInsert: function () {
      var insertRow = this._createInsertRow();
      this._insertRow.replaceWith(insertRow);
      this._insertRow = insertRow;
      this.refresh();
    },

    editItem: function (item) {
      var $row = this.rowByItem(item);
      if ($row.length) {
        this._editRow($row);
      }
    },

    rowByItem: function (item) {
      if (item.jquery || item.nodeType) return $(item);

      return this._content.find("tr").filter(function () {
        return $.data(this, JSGRID_ROW_DATA_KEY) === item;
      });
    },

    _editRow: function ($row) {
      if (!this.editing) return;

      var item = $row.data(JSGRID_ROW_DATA_KEY);

      var args = this._callEventHandler(this.onItemEditing, {
        row: $row,
        item: item,
        itemIndex: this._itemIndex(item),
      });

      if (args.cancel) return;

      if (this._editingRow) {
        this.cancelEdit();
      }

      var $editRow = this._createEditRow(item);

      this._editingRow = $row;
      $row.hide();
      $editRow.insertBefore($row);
      $row.data(JSGRID_EDIT_ROW_DATA_KEY, $editRow);
    },

    _createEditRow: function (item) {
      if ($.isFunction(this.editRowRenderer)) {
        return $(
          this.renderTemplate(this.editRowRenderer, this, {
            item: item,
            itemIndex: this._itemIndex(item),
          })
        );
      }

      var $result = $("<tr>").addClass(this.editRowClass);

      this._eachField(function (field) {
        var fieldValue = this._getItemFieldValue(item, field);

        this._prepareCell("<td>", field, "editcss")
          .append(
            this.renderTemplate(field.editTemplate || "", field, {
              value: fieldValue,
              item: item,
            })
          )
          .appendTo($result);
      });

      return $result;
    },

    updateItem: function (item, editedItem) {
      if (arguments.length === 1) {
        editedItem = item;
      }

      var $row = item ? this.rowByItem(item) : this._editingRow;
      editedItem = editedItem || this._getValidatedEditedItem();

      if (!editedItem) return;

      return this._updateRow($row, editedItem);
    },

    _getValidatedEditedItem: function () {
      var item = this._getEditedItem();
      return this._validateItem(item, this._getEditRow()) ? item : null;
    },

    _updateRow: function ($updatingRow, editedItem) {
      var updatingItem = $updatingRow.data(JSGRID_ROW_DATA_KEY),
        updatingItemIndex = this._itemIndex(updatingItem),
        updatedItem = $.extend(true, {}, updatingItem, editedItem);

      var args = this._callEventHandler(this.onItemUpdating, {
        row: $updatingRow,
        item: updatedItem,
        itemIndex: updatingItemIndex,
        previousItem: updatingItem,
      });

      return this._controllerCall(
        "updateItem",
        updatedItem,
        args.cancel,
        function (loadedUpdatedItem) {
          var previousItem = $.extend(true, {}, updatingItem);
          updatedItem =
            loadedUpdatedItem || $.extend(true, updatingItem, editedItem);

          var $updatedRow = this._finishUpdate(
            $updatingRow,
            updatedItem,
            updatingItemIndex
          );

          this._callEventHandler(this.onItemUpdated, {
            row: $updatedRow,
            item: updatedItem,
            itemIndex: updatingItemIndex,
            previousItem: previousItem,
          });
        }
      );
    },

    _rowIndex: function (row) {
      return this._content.children().index($(row));
    },

    _itemIndex: function (item) {
      return $.inArray(item, this.data);
    },

    _finishUpdate: function ($updatingRow, updatedItem, updatedItemIndex) {
      this.cancelEdit();
      this.data[updatedItemIndex] = updatedItem;

      var $updatedRow = this._createRow(updatedItem, updatedItemIndex);
      $updatingRow.replaceWith($updatedRow);
      return $updatedRow;
    },

    _getEditedItem: function () {
      var result = {};
      this._eachField(function (field) {
        if (field.editing) {
          this._setItemFieldValue(result, field, field.editValue());
        }
      });
      return result;
    },

    cancelEdit: function () {
      if (!this._editingRow) return;

      this._getEditRow().remove();
      this._editingRow.show();
      this._editingRow = null;
    },

    _getEditRow: function () {
      return (
        this._editingRow && this._editingRow.data(JSGRID_EDIT_ROW_DATA_KEY)
      );
    },

    deleteItem: function (item) {
      var $row = this.rowByItem(item);

      if (!$row.length) return;

      if (
        this.confirmDeleting &&
        !window.confirm(
          getOrApply(this.deleteConfirm, this, $row.data(JSGRID_ROW_DATA_KEY))
        )
      )
        return;

      return this._deleteRow($row);
    },

    _deleteRow: function ($row) {
      var deletingItem = $row.data(JSGRID_ROW_DATA_KEY),
        deletingItemIndex = this._itemIndex(deletingItem);

      var args = this._callEventHandler(this.onItemDeleting, {
        row: $row,
        item: deletingItem,
        itemIndex: deletingItemIndex,
      });

      return this._controllerCall(
        "deleteItem",
        deletingItem,
        args.cancel,
        function () {
          this._loadStrategy.finishDelete(deletingItem, deletingItemIndex);

          this._callEventHandler(this.onItemDeleted, {
            row: $row,
            item: deletingItem,
            itemIndex: deletingItemIndex,
          });
        }
      );
    },
  };

  $.fn.jsGrid = function (config) {
    var args = $.makeArray(arguments),
      methodArgs = args.slice(1),
      result = this;

    this.each(function () {
      var $element = $(this),
        instance = $element.data(JSGRID_DATA_KEY),
        methodResult;

      if (instance) {
        if (typeof config === "string") {
          methodResult = instance[config].apply(instance, methodArgs);
          if (methodResult !== undefined && methodResult !== instance) {
            result = methodResult;
            return false;
          }
        } else {
          instance._detachWindowResizeCallback();
          instance._init(config);
          instance.render();
        }
      } else {
        new Grid($element, config);
      }
    });

    return result;
  };

  var fields = {};

  var setDefaults = function (config) {
    var componentPrototype;

    if ($.isPlainObject(config)) {
      componentPrototype = Grid.prototype;
    } else {
      componentPrototype = fields[config].prototype;
      config = arguments[1] || {};
    }

    $.extend(componentPrototype, config);
  };

  var locales = {};

  var locale = function (lang) {
    var localeConfig = $.isPlainObject(lang) ? lang : locales[lang];

    if (!localeConfig) throw Error("unknown locale " + lang);

    setLocale(jsGrid, localeConfig);
  };

  var setLocale = function (obj, localeConfig) {
    $.each(localeConfig, function (field, value) {
      if ($.isPlainObject(value)) {
        setLocale(
          obj[field] || obj[field[0].toUpperCase() + field.slice(1)],
          value
        );
        return;
      }

      if (obj.hasOwnProperty(field)) {
        obj[field] = value;
      } else {
        obj.prototype[field] = value;
      }
    });
  };

  window.jsGrid = {
    Grid: Grid,
    fields: fields,
    setDefaults: setDefaults,
    locales: locales,
    locale: locale,
    version: "1.5.3",
  };
})(window, jQuery);

(function (jsGrid, $, undefined) {
  function LoadIndicator(config) {
    this._init(config);
  }

  LoadIndicator.prototype = {
    container: "body",
    message: "Loading...",
    shading: true,

    zIndex: 1000,
    shaderClass: "jsgrid-load-shader",
    loadPanelClass: "jsgrid-load-panel",

    _init: function (config) {
      $.extend(true, this, config);

      this._initContainer();
      this._initShader();
      this._initLoadPanel();
    },

    _initContainer: function () {
      this._container = $(this.container);
    },

    _initShader: function () {
      if (!this.shading) return;

      this._shader = $("<div>")
        .addClass(this.shaderClass)
        .hide()
        .css({
          position: "absolute",
          top: 0,
          right: 0,
          bottom: 0,
          left: 0,
          zIndex: this.zIndex,
        })
        .appendTo(this._container);
    },

    _initLoadPanel: function () {
      this._loadPanel = $("<div>")
        .addClass(this.loadPanelClass)
        .text(this.message)
        .hide()
        .css({
          position: "absolute",
          top: "50%",
          left: "50%",
          zIndex: this.zIndex,
        })
        .appendTo(this._container);
    },

    show: function () {
      var $loadPanel = this._loadPanel.show();

      var actualWidth = $loadPanel.outerWidth();
      var actualHeight = $loadPanel.outerHeight();

      $loadPanel.css({
        marginTop: -actualHeight / 2,
        marginLeft: -actualWidth / 2,
      });

      this._shader.show();
    },

    hide: function () {
      this._loadPanel.hide();
      this._shader.hide();
    },
  };

  jsGrid.LoadIndicator = LoadIndicator;
})(jsGrid, jQuery);

(function (jsGrid, $, undefined) {
  function DirectLoadingStrategy(grid) {
    this._grid = grid;
  }

  DirectLoadingStrategy.prototype = {
    firstDisplayIndex: function () {
      var grid = this._grid;
      return grid.option("paging")
        ? (grid.option("pageIndex") - 1) * grid.option("pageSize")
        : 0;
    },

    lastDisplayIndex: function () {
      var grid = this._grid;
      var itemsCount = grid.option("data").length;

      return grid.option("paging")
        ? Math.min(
            grid.option("pageIndex") * grid.option("pageSize"),
            itemsCount
          )
        : itemsCount;
    },

    itemsCount: function () {
      return this._grid.option("data").length;
    },

    openPage: function (index) {
      this._grid.refresh();
    },

    loadParams: function () {
      return {};
    },

    sort: function () {
      this._grid._sortData();
      this._grid.refresh();
      return $.Deferred().resolve().promise();
    },

    reset: function () {
      this._grid.refresh();
      return $.Deferred().resolve().promise();
    },

    finishLoad: function (loadedData) {
      this._grid.option("data", loadedData);
    },

    finishInsert: function (insertedItem) {
      var grid = this._grid;
      grid.option("data").push(insertedItem);
      grid.refresh();
    },

    finishDelete: function (deletedItem, deletedItemIndex) {
      var grid = this._grid;
      grid.option("data").splice(deletedItemIndex, 1);
      grid.reset();
    },
  };

  function PageLoadingStrategy(grid) {
    this._grid = grid;
    this._itemsCount = 0;
  }

  PageLoadingStrategy.prototype = {
    firstDisplayIndex: function () {
      return 0;
    },

    lastDisplayIndex: function () {
      return this._grid.option("data").length;
    },

    itemsCount: function () {
      return this._itemsCount;
    },

    openPage: function (index) {
      this._grid.loadData();
    },

    loadParams: function () {
      var grid = this._grid;
      return {
        pageIndex: grid.option("pageIndex"),
        pageSize: grid.option("pageSize"),
      };
    },

    reset: function () {
      return this._grid.loadData();
    },

    sort: function () {
      return this._grid.loadData();
    },

    finishLoad: function (loadedData) {
      this._itemsCount = loadedData.itemsCount;
      this._grid.option("data", loadedData.data);
    },

    finishInsert: function (insertedItem) {
      this._grid.search();
    },

    finishDelete: function (deletedItem, deletedItemIndex) {
      this._grid.search();
    },
  };

  jsGrid.loadStrategies = {
    DirectLoadingStrategy: DirectLoadingStrategy,
    PageLoadingStrategy: PageLoadingStrategy,
  };
})(jsGrid, jQuery);

(function (jsGrid, $, undefined) {
  var isDefined = function (val) {
    return typeof val !== "undefined" && val !== null;
  };

  var sortStrategies = {
    string: function (str1, str2) {
      if (!isDefined(str1) && !isDefined(str2)) return 0;

      if (!isDefined(str1)) return -1;

      if (!isDefined(str2)) return 1;

      return ("" + str1).localeCompare("" + str2);
    },

    number: function (n1, n2) {
      return n1 - n2;
    },

    date: function (dt1, dt2) {
      return dt1 - dt2;
    },

    numberAsString: function (n1, n2) {
      return parseFloat(n1) - parseFloat(n2);
    },
  };

  jsGrid.sortStrategies = sortStrategies;
})(jsGrid, jQuery);

(function (jsGrid, $, undefined) {
  function Validation(config) {
    this._init(config);
  }

  Validation.prototype = {
    _init: function (config) {
      $.extend(true, this, config);
    },

    validate: function (args) {
      var errors = [];

      $.each(this._normalizeRules(args.rules), function (_, rule) {
        if (rule.validator(args.value, args.item, rule.param)) return;

        var errorMessage = $.isFunction(rule.message)
          ? rule.message(args.value, args.item)
          : rule.message;
        errors.push(errorMessage);
      });

      return errors;
    },

    _normalizeRules: function (rules) {
      if (!$.isArray(rules)) rules = [rules];

      return $.map(
        rules,
        $.proxy(function (rule) {
          return this._normalizeRule(rule);
        }, this)
      );
    },

    _normalizeRule: function (rule) {
      if (typeof rule === "string") rule = { validator: rule };

      if ($.isFunction(rule)) rule = { validator: rule };

      if ($.isPlainObject(rule)) rule = $.extend({}, rule);
      else throw Error("wrong validation config specified");

      if ($.isFunction(rule.validator)) return rule;

      return this._applyNamedValidator(rule, rule.validator);
    },

    _applyNamedValidator: function (rule, validatorName) {
      delete rule.validator;

      var validator = validators[validatorName];
      if (!validator) throw Error('unknown validator "' + validatorName + '"');

      if ($.isFunction(validator)) {
        validator = { validator: validator };
      }

      return $.extend({}, validator, rule);
    },
  };

  jsGrid.Validation = Validation;

  var validators = {
    required: {
      message: "Field is required",
      validator: function (value) {
        return value !== undefined && value !== null && value !== "";
      },
    },

    rangeLength: {
      message: "Field value length is out of the defined range",
      validator: function (value, _, param) {
        return value.length >= param[0] && value.length <= param[1];
      },
    },

    minLength: {
      message: "Field value is too short",
      validator: function (value, _, param) {
        return value.length >= param;
      },
    },

    maxLength: {
      message: "Field value is too long",
      validator: function (value, _, param) {
        return value.length <= param;
      },
    },

    pattern: {
      message: "Field value is not matching the defined pattern",
      validator: function (value, _, param) {
        if (typeof param === "string") {
          param = new RegExp("^(?:" + param + ")$");
        }
        return param.test(value);
      },
    },

    range: {
      message: "Field value is out of the defined range",
      validator: function (value, _, param) {
        return value >= param[0] && value <= param[1];
      },
    },

    min: {
      message: "Field value is too small",
      validator: function (value, _, param) {
        return value >= param;
      },
    },

    max: {
      message: "Field value is too large",
      validator: function (value, _, param) {
        return value <= param;
      },
    },
  };

  jsGrid.validators = validators;
})(jsGrid, jQuery);

(function (jsGrid, $, undefined) {
  function Field(config) {
    $.extend(true, this, config);
    this.sortingFunc = this._getSortingFunc();
  }

  Field.prototype = {
    name: "",
    title: null,
    css: "",
    align: "",
    width: 100,

    visible: true,
    filtering: true,
    inserting: true,
    editing: true,
    sorting: true,
    sorter: "string", // name of SortStrategy or function to compare elements

    headerTemplate: function () {
      return this.title === undefined || this.title === null
        ? this.name
        : this.title;
    },

    itemTemplate: function (value, item) {
      return value;
    },

    filterTemplate: function () {
      return "";
    },

    insertTemplate: function () {
      return "";
    },

    editTemplate: function (value, item) {
      this._value = value;
      return this.itemTemplate(value, item);
    },

    filterValue: function () {
      return "";
    },

    insertValue: function () {
      return "";
    },

    editValue: function () {
      return this._value;
    },

    _getSortingFunc: function () {
      var sorter = this.sorter;

      if ($.isFunction(sorter)) {
        return sorter;
      }

      if (typeof sorter === "string") {
        return jsGrid.sortStrategies[sorter];
      }

      throw Error('wrong sorter for the field "' + this.name + '"!');
    },
  };

  jsGrid.Field = Field;
})(jsGrid, jQuery);

(function (jsGrid, $, undefined) {
  var Field = jsGrid.Field;

  function TextField(config) {
    Field.call(this, config);
  }

  TextField.prototype = new Field({
    autosearch: true,
    readOnly: false,

    filterTemplate: function () {
      if (!this.filtering) return "";

      var grid = this._grid,
        $result = (this.filterControl = this._createTextBox());

      if (this.autosearch) {
        $result.on("keypress", function (e) {
          if (e.which === 13) {
            grid.search();
            e.preventDefault();
          }
        });
      }

      return $result;
    },

    insertTemplate: function () {
      if (!this.inserting) return "";

      return (this.insertControl = this._createTextBox());
    },

    editTemplate: function (value) {
      if (!this.editing) return this.itemTemplate.apply(this, arguments);

      var $result = (this.editControl = this._createTextBox());
      $result.val(value);
      return $result;
    },

    filterValue: function () {
      return this.filterControl.val();
    },

    insertValue: function () {
      return this.insertControl.val();
    },

    editValue: function () {
      return this.editControl.val();
    },

    _createTextBox: function () {
      return $("<input>")
        .attr("type", "text")
        .prop("readonly", !!this.readOnly);
    },
  });

  jsGrid.fields.text = jsGrid.TextField = TextField;
})(jsGrid, jQuery);

(function (jsGrid, $, undefined) {
  var TextField = jsGrid.TextField;

  function NumberField(config) {
    TextField.call(this, config);
  }

  NumberField.prototype = new TextField({
    sorter: "number",
    align: "right",
    readOnly: false,

    filterValue: function () {
      return this.filterControl.val()
        ? parseInt(this.filterControl.val() || 0, 10)
        : undefined;
    },

    insertValue: function () {
      return this.insertControl.val()
        ? parseInt(this.insertControl.val() || 0, 10)
        : undefined;
    },

    editValue: function () {
      return this.editControl.val()
        ? parseInt(this.editControl.val() || 0, 10)
        : undefined;
    },

    _createTextBox: function () {
      return $("<input>")
        .attr("type", "number")
        .prop("readonly", !!this.readOnly);
    },
  });

  jsGrid.fields.number = jsGrid.NumberField = NumberField;
})(jsGrid, jQuery);

(function (jsGrid, $, undefined) {
  var TextField = jsGrid.TextField;

  function TextAreaField(config) {
    TextField.call(this, config);
  }

  TextAreaField.prototype = new TextField({
    insertTemplate: function () {
      if (!this.inserting) return "";

      return (this.insertControl = this._createTextArea());
    },

    editTemplate: function (value) {
      if (!this.editing) return this.itemTemplate.apply(this, arguments);

      var $result = (this.editControl = this._createTextArea());
      $result.val(value);
      return $result;
    },

    _createTextArea: function () {
      return $("<textarea>").prop("readonly", !!this.readOnly);
    },
  });

  jsGrid.fields.textarea = jsGrid.TextAreaField = TextAreaField;
})(jsGrid, jQuery);

(function (jsGrid, $, undefined) {
  var NumberField = jsGrid.NumberField;
  var numberValueType = "number";
  var stringValueType = "string";

  function SelectField(config) {
    this.items = [];
    this.selectedIndex = -1;
    this.valueField = "";
    this.textField = "";

    if (config.valueField && config.items.length) {
      var firstItemValue = config.items[0][config.valueField];
      this.valueType =
        typeof firstItemValue === numberValueType
          ? numberValueType
          : stringValueType;
    }

    this.sorter = this.valueType;

    NumberField.call(this, config);
  }

  SelectField.prototype = new NumberField({
    align: "center",
    valueType: numberValueType,

    itemTemplate: function (value) {
      var items = this.items,
        valueField = this.valueField,
        textField = this.textField,
        resultItem;

      if (valueField) {
        resultItem =
          $.grep(items, function (item, index) {
            return item[valueField] === value;
          })[0] || {};
      } else {
        resultItem = items[value];
      }

      var result = textField ? resultItem[textField] : resultItem;

      return result === undefined || result === null ? "" : result;
    },

    filterTemplate: function () {
      if (!this.filtering) return "";

      var grid = this._grid,
        $result = (this.filterControl = this._createSelect());

      if (this.autosearch) {
        $result.on("change", function (e) {
          grid.search();
        });
      }

      return $result;
    },

    insertTemplate: function () {
      if (!this.inserting) return "";

      return (this.insertControl = this._createSelect());
    },

    editTemplate: function (value) {
      if (!this.editing) return this.itemTemplate.apply(this, arguments);

      var $result = (this.editControl = this._createSelect());
      value !== undefined && $result.val(value);
      return $result;
    },

    filterValue: function () {
      var val = this.filterControl.val();
      return this.valueType === numberValueType ? parseInt(val || 0, 10) : val;
    },

    insertValue: function () {
      var val = this.insertControl.val();
      return this.valueType === numberValueType ? parseInt(val || 0, 10) : val;
    },

    editValue: function () {
      var val = this.editControl.val();
      return this.valueType === numberValueType ? parseInt(val || 0, 10) : val;
    },

    _createSelect: function () {
      var $result = $("<select>"),
        valueField = this.valueField,
        textField = this.textField,
        selectedIndex = this.selectedIndex;

      $.each(this.items, function (index, item) {
        var value = valueField ? item[valueField] : index,
          text = textField ? item[textField] : item;

        var $option = $("<option>")
          .attr("value", value)
          .text(text)
          .appendTo($result);

        $option.prop("selected", selectedIndex === index);
      });

      $result.prop("disabled", !!this.readOnly);

      return $result;
    },
  });

  jsGrid.fields.select = jsGrid.SelectField = SelectField;
})(jsGrid, jQuery);

(function (jsGrid, $, undefined) {
  var Field = jsGrid.Field;

  function CheckboxField(config) {
    Field.call(this, config);
  }

  CheckboxField.prototype = new Field({
    sorter: "number",
    align: "center",
    autosearch: true,

    itemTemplate: function (value) {
      return this._createCheckbox().prop({
        checked: value,
        disabled: true,
      });
    },

    filterTemplate: function () {
      if (!this.filtering) return "";

      var grid = this._grid,
        $result = (this.filterControl = this._createCheckbox());

      $result.prop({
        readOnly: true,
        indeterminate: true,
      });

      $result.on("click", function () {
        var $cb = $(this);

        if ($cb.prop("readOnly")) {
          $cb.prop({
            checked: false,
            readOnly: false,
          });
        } else if (!$cb.prop("checked")) {
          $cb.prop({
            readOnly: true,
            indeterminate: true,
          });
        }
      });

      if (this.autosearch) {
        $result.on("click", function () {
          grid.search();
        });
      }

      return $result;
    },

    insertTemplate: function () {
      if (!this.inserting) return "";

      return (this.insertControl = this._createCheckbox());
    },

    editTemplate: function (value) {
      if (!this.editing) return this.itemTemplate.apply(this, arguments);

      var $result = (this.editControl = this._createCheckbox());
      $result.prop("checked", value);
      return $result;
    },

    filterValue: function () {
      return this.filterControl.get(0).indeterminate
        ? undefined
        : this.filterControl.is(":checked");
    },

    insertValue: function () {
      return this.insertControl.is(":checked");
    },

    editValue: function () {
      return this.editControl.is(":checked");
    },

    _createCheckbox: function () {
      return $("<input>").attr("type", "checkbox");
    },
  });

  jsGrid.fields.checkbox = jsGrid.CheckboxField = CheckboxField;
})(jsGrid, jQuery);

(function (jsGrid, $, undefined) {
  var Field = jsGrid.Field;

  function ControlField(config) {
    Field.call(this, config);
    this._configInitialized = false;
  }

  ControlField.prototype = new Field({
    css: "jsgrid-control-field",
    align: "center",
    width: 50,
    filtering: false,
    inserting: false,
    editing: false,
    sorting: false,

    buttonClass: "jsgrid-button",
    modeButtonClass: "jsgrid-mode-button",

    modeOnButtonClass: "jsgrid-mode-on-button",
    searchModeButtonClass: "jsgrid-search-mode-button",
    insertModeButtonClass: "jsgrid-insert-mode-button",
    editButtonClass: "jsgrid-edit-button",
    deleteButtonClass: "jsgrid-delete-button",
    searchButtonClass: "jsgrid-search-button",
    clearFilterButtonClass: "jsgrid-clear-filter-button",
    insertButtonClass: "jsgrid-insert-button",
    updateButtonClass: "jsgrid-update-button",
    cancelEditButtonClass: "jsgrid-cancel-edit-button",

    searchModeButtonTooltip: "Switch to searching",
    insertModeButtonTooltip: "Switch to inserting",
    editButtonTooltip: "Edit",
    deleteButtonTooltip: "Delete",
    searchButtonTooltip: "Search",
    clearFilterButtonTooltip: "Clear filter",
    insertButtonTooltip: "Insert",
    updateButtonTooltip: "Update",
    cancelEditButtonTooltip: "Cancel edit",

    editButton: true,
    deleteButton: true,
    clearFilterButton: true,
    modeSwitchButton: true,

    _initConfig: function () {
      this._hasFiltering = this._grid.filtering;
      this._hasInserting = this._grid.inserting;

      if (this._hasInserting && this.modeSwitchButton) {
        this._grid.inserting = false;
      }

      this._configInitialized = true;
    },

    headerTemplate: function () {
      if (!this._configInitialized) {
        this._initConfig();
      }

      var hasFiltering = this._hasFiltering;
      var hasInserting = this._hasInserting;

      if (!this.modeSwitchButton || (!hasFiltering && !hasInserting)) return "";

      if (hasFiltering && !hasInserting)
        return this._createFilterSwitchButton();

      if (hasInserting && !hasFiltering)
        return this._createInsertSwitchButton();

      return this._createModeSwitchButton();
    },

    itemTemplate: function (value, item) {
      var $result = $([]);

      if (this.editButton) {
        $result = $result.add(this._createEditButton(item));
      }

      if (this.deleteButton) {
        $result = $result.add(this._createDeleteButton(item));
      }

      return $result;
    },

    filterTemplate: function () {
      var $result = this._createSearchButton();
      return this.clearFilterButton
        ? $result.add(this._createClearFilterButton())
        : $result;
    },

    insertTemplate: function () {
      return this._createInsertButton();
    },

    editTemplate: function () {
      return this._createUpdateButton().add(this._createCancelEditButton());
    },

    _createFilterSwitchButton: function () {
      return this._createOnOffSwitchButton(
        "filtering",
        this.searchModeButtonClass,
        true
      );
    },

    _createInsertSwitchButton: function () {
      return this._createOnOffSwitchButton(
        "inserting",
        this.insertModeButtonClass,
        false
      );
    },

    _createOnOffSwitchButton: function (option, cssClass, isOnInitially) {
      var isOn = isOnInitially;

      var updateButtonState = $.proxy(function () {
        $button.toggleClass(this.modeOnButtonClass, isOn);
      }, this);

      var $button = this._createGridButton(
        this.modeButtonClass + " " + cssClass,
        "",
        function (grid) {
          isOn = !isOn;
          grid.option(option, isOn);
          updateButtonState();
        }
      );

      updateButtonState();

      return $button;
    },

    _createModeSwitchButton: function () {
      var isInserting = false;

      var updateButtonState = $.proxy(function () {
        $button
          .attr(
            "title",
            isInserting
              ? this.searchModeButtonTooltip
              : this.insertModeButtonTooltip
          )
          .toggleClass(this.insertModeButtonClass, !isInserting)
          .toggleClass(this.searchModeButtonClass, isInserting);
      }, this);

      var $button = this._createGridButton(
        this.modeButtonClass,
        "",
        function (grid) {
          isInserting = !isInserting;
          grid.option("inserting", isInserting);
          grid.option("filtering", !isInserting);
          updateButtonState();
        }
      );

      updateButtonState();

      return $button;
    },

    _createEditButton: function (item) {
      return this._createGridButton(
        this.editButtonClass,
        this.editButtonTooltip,
        function (grid, e) {
          grid.editItem(item);
          e.stopPropagation();
        }
      );
    },

    _createDeleteButton: function (item) {
      return this._createGridButton(
        this.deleteButtonClass,
        this.deleteButtonTooltip,
        function (grid, e) {
          grid.deleteItem(item);
          e.stopPropagation();
        }
      );
    },

    _createSearchButton: function () {
      return this._createGridButton(
        this.searchButtonClass,
        this.searchButtonTooltip,
        function (grid) {
          grid.search();
        }
      );
    },

    _createClearFilterButton: function () {
      return this._createGridButton(
        this.clearFilterButtonClass,
        this.clearFilterButtonTooltip,
        function (grid) {
          grid.clearFilter();
        }
      );
    },

    _createInsertButton: function () {
      return this._createGridButton(
        this.insertButtonClass,
        this.insertButtonTooltip,
        function (grid) {
          grid.insertItem().done(function () {
            grid.clearInsert();
          });
        }
      );
    },

    _createUpdateButton: function () {
      return this._createGridButton(
        this.updateButtonClass,
        this.updateButtonTooltip,
        function (grid, e) {
          grid.updateItem();
          e.stopPropagation();
        }
      );
    },

    _createCancelEditButton: function () {
      return this._createGridButton(
        this.cancelEditButtonClass,
        this.cancelEditButtonTooltip,
        function (grid, e) {
          grid.cancelEdit();
          e.stopPropagation();
        }
      );
    },

    _createGridButton: function (cls, tooltip, clickHandler) {
      var grid = this._grid;

      return $("<input>")
        .addClass(this.buttonClass)
        .addClass(cls)
        .attr({
          type: "button",
          title: tooltip,
        })
        .on("click", function (e) {
          clickHandler(grid, e);
        });
    },

    editValue: function () {
      return "";
    },
  });

  jsGrid.fields.control = jsGrid.ControlField = ControlField;
})(jsGrid, jQuery);
