text: /*
   * 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 css='width:50px;height:50px;overflow:hidden;position:absolute;top:-10000px;left:-10000px;'></div>");
                      var $ghostContent = $("<div css='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));
  