Procházet zdrojové kódy

added new user management
new user management css classes
new js diff function
jsgrid framework files

CauseFX před 5 roky
rodič
revize
ae4bc6b91d

+ 144 - 2
api/pages/settings-user-manage-users.php

@@ -13,14 +13,156 @@ function get_page_settings_user_manage_users($Organizr)
 	}
 	return '
 <script>
-    buildUserManagement();
+	$(function() {
+	    $("#jsGrid-Users").jsGrid({
+	        height: "auto",
+	        width: "100%",
+	 		loadIndication: true,
+		    loadIndicationDelay: 50000,
+		    loadMessage: "Please, wait...",
+		    noDataContent: "Loading... or Not found",
+		    loadShading: true,
+	        filtering: false,
+	        editing: true,
+	        sorting: true,
+	        paging: true,
+	        autoload: true,
+	        selecting: true,
+	 		confirmDeleting: false,
+	        pageSize: 10,
+	        pageButtonCount: 5,
+        	pagerFormat: "&nbsp;&nbsp; {first} {prev} {pages} {next} {last} &nbsp;&nbsp;",
+	        controller: {
+	            loadData: function() {
+	                let d = $.Deferred();
+	                $.ajax({
+	                    url: "api/v2/users?includeGroups",
+	                    dataType: "json"
+	                }).done(function(response) {
+	                	let groupObj = response.response.data.groups;
+	                	$("#jsGrid-Users").jsGrid("fieldOption", "group_id", "items", groupObj);
+	                    d.resolve(response.response.data.users);
+	                });
+	                return d.promise();
+	            }
+	        },
+	 
+	        fields: [
+	        	{ name: "image", title: "Avatar", type: "text", width: 45, css: "text-center hidden-xs", validate: "required",
+	                itemTemplate: function(value) {
+	                    return \'<img alt="user-img" class="img-circle" src="\'+value+\'" width="45">\'; }
+	            },
+	            { name: "username", type: "text", title: "Username", validate: "required", width: 150},
+	            { name: "email", type: "text", title: "Email", validate: "required", width: 200},
+	            { name: "register_date", type: "text", title: "Date Registered",editing: false, css: "hidden-xs",
+	            	itemTemplate: function(value) {
+	                    return moment(value).format(\'ll\') + \' \' + moment(value).format(\'LT\') },
+	            },
+	            { name: "group_id", type: "select", title: "Group", validate: "required",
+	            	items: [],
+				    valueField: "group_id",
+				    textField: "group"
+	            },
+	            { name: "locked", title: "Locked", type: "select", width: 45, validate: "required",
+	            	itemTemplate: function(value) {
+	                    return (value == 0 || value == null || value == "" || value == " ") ? "No" : "Yes"; },
+	                items: [
+	                	{ Name: "No", Id: 0 },
+         				{ Name: "Yes", Id: 1 },
+	                ],
+				    valueField: "Id",
+    				textField: "Name"
+    				
+	            },
+	            { name: "password", type: "text", title: "Password", css: "text-center",
+	                itemTemplate: function(value) {
+	                    return "<i class=\"mdi mdi-account-key\"></i>"; },
+	            	editTemplate: function(item, value) {
+	            	var $result = jsGrid.fields.text.prototype.editTemplate.apply(this, arguments);
+	                    return $result; }
+	             
+	            },
+	            { type: "control", modeSwitchButton: false, editButton: false, title: "Action",
+		             headerTemplate: function() {
+	                    return "Action";
+	                }
+	             }
+	        ],
+		    onItemDeleting: function(args) {
+		        if(args.item.protected) {
+		            args.cancel = true;
+		        }
+		        args.cancel = true;
+		        let id = args.item.id;
+		        swal({
+			        title: window.lang.translate("Delete ")+args.item.username+"?",
+			        icon: "warning",
+			        buttons: {
+			            cancel: window.lang.translate("No"),
+			            confirm: window.lang.translate("Yes"),
+			        },
+			        dangerMode: true,
+			        className: "bg-org",
+			        confirmButtonColor: "#DD6B55"
+			    }).then(function(willDelete) {
+			        if (willDelete) {
+				        organizrAPI2("DELETE","api/v2/users/" + id, null,true).success(function(data) {
+					        $("#jsGrid-Users").jsGrid("render");
+				        	message("User Deleted","",activeInfo.settings.notifications.position,"#FFF","success","5000");
+				        }).fail(function(xhr) {
+					        message("User Deleted Error", xhr.responseJSON.response.message, activeInfo.settings.notifications.position, "#FFF", "error", "10000");
+					        console.error("Organizr Function: API Connection Failed");
+				        });
+					}
+				});
+		    },
+		    onItemUpdating: function(args) {
+		        if(typeof args.item.id == "undefined"){
+		        	args.cancel = true;
+		            alert("Could not get ID");
+		        }
+		        let diff = objDiff(args.previousItem,args.item);
+		        let id = args.item.id;
+		        organizrAPI2("PUT","api/v2/users/" + id, diff,true).success(function(data) {
+					try {
+						let response = data.response;
+						$("#jsGrid-Users").jsGrid("render");
+						message("User Updated",response.message,activeInfo.settings.notifications.position,"#FFF","success","5000");
+					}catch(e) {
+						console.log(e + " error: " + data);
+						orgErrorAlert("<h4>" + e + "</h4>" + formatDebug(data));
+						return false;
+					}
+				
+				}).fail(function(xhr) {
+					message("User Error", xhr.responseJSON.response.message, activeInfo.settings.notifications.position,"#FFF","error","10000");
+					console.error("Organizr Function: API Connection Failed");
+				});
+		    },
+		    
+		    onRefreshed: function(){
+				$(".jsgrid-pager").addClass( "pull-right" );
+				$(".jsgrid-pager").find(".jsgrid-pager-page a").addClass( "btn btn-info" );
+				$(".jsgrid-pager").find(".jsgrid-pager-nav-button a").addClass( "btn btn-info" );
+				$(".jsgrid-pager").find(".jsgrid-pager-current-page").addClass( "btn btn-info m-r-5" );
+				let nav = $(".jsgrid-pager").find(".jsgrid-pager-nav-button");
+				$.each(nav, function(i,v) {
+					if(v.innerText === "..."){
+						$(this).addClass("hidden");
+					}
+				})
+			}
+	    });
+	});
+    //buildUserManagement();
 </script>
 <div class="panel bg-org panel-info">
     <div class="panel-heading">
         <span lang="en">MANAGE USERS</span>
         <button type="button" class="btn btn-info btn-circle pull-right popup-with-form" href="#new-user-form" data-effect="mfp-3d-unfold"><i class="fa fa-plus"></i> </button>
     </div>
-    <div class="table-responsive">
+    <div id="jsGrid-Users" class=""></div>
+    <div class="table-responsive hidden">
         <table class="table table-hover manage-u-table">
             <thead>
                 <tr>

+ 50 - 0
css/organizr.css

@@ -1,3 +1,53 @@
+/*JSGrid*/
+.jsgrid-grid-body {
+    overflow-x: auto;
+    overflow-y: auto;
+    -webkit-overflow-scrolling: touch;
+}
+.jsgrid-header-row .jsgrid-header-sort {
+    background: #232323;
+}
+.jsgrid-edit-row>td {
+    background: #3e3d3d;
+}
+.jsgrid-table {
+    width: 100% !important;
+    table-layout: fixed;
+    border-collapse: collapse;
+    border-spacing: 0;
+}
+.jsgrid-grid-header {
+    overflow-y: auto;
+}
+.jsgrid-grid-header {
+    background: transparent;
+}
+.jsgrid-nodata-row td {
+    background: #181818;
+}
+.jsgrid-grid-body td {
+    border: transparent;
+}
+.jsgrid-grid-body td {
+    border-top: 1px solid #232323 !important;
+}
+tr.jsgrid-header-row th {
+    border: transparent !important;
+}
+.jsgrid-selected-row td {
+    background: #232323 !important;
+}
+.jsgrid-pager a {
+    color: white;
+}
+span.jsgrid-pager-page.jsgrid-pager-current-page.btn.btn-info{
+    padding: 6px 11px;
+    margin-left: 9px;
+    margin-right: 9px !important;
+}
+.bg-org .swal-title {
+    color: white;
+}
 /*Preloader*/
 
 .preloader {

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
css/organizr.min.css


+ 3 - 0
index.php

@@ -34,6 +34,8 @@ $Organizr = new Organizr();
     <link href="plugins/bower_components/owl.carousel/owl.carousel.min.css" rel="stylesheet" type="text/css"/>
     <link href="plugins/bower_components/owl.carousel/owl.theme.default.css" rel="stylesheet" type="text/css"/>
     <link href="plugins/bower_components/hover/hover-min.css" rel="stylesheet" type="text/css"/>
+    <link href="plugins/bower_components/jsgrid/dist/jsgrid.min.css" rel="stylesheet" type="text/css"/>
+    <link href="plugins/bower_components/jsgrid/dist/jsgrid-theme.min.css" rel="stylesheet" type="text/css"/>
     <link href="css/animate.css" rel="stylesheet">
     <link href="css/simplebar.css" rel="stylesheet">
     <link href="css/plyr.css" rel="stylesheet">
@@ -218,6 +220,7 @@ $Organizr = new Organizr();
 <script src="plugins/bower_components/mousetrap/mousetrap.min.js"></script>
 <script src="plugins/bower_components/bootstrap-treeview-master/dist/bootstrap-treeview.min.js"></script>
 <script src="plugins/bower_components/jquery.easy-pie-chart/dist/jquery.easypiechart.min.js"></script>
+<script src="plugins/bower_components/jsgrid/dist/jsgrid.min.js"></script>
 <script src="js/gauge.min.js"></script>
 <script src="js/jquery.mousewheel.min.js"></script>
 <script src="js/ua-parser.min.js"></script>

+ 120 - 0
js/functions.js

@@ -9818,6 +9818,126 @@ function checkToken(activate = false){
         }
     }
 }
+function objDiff(obj1, obj2) {
+
+	// Make sure an object to compare is provided
+	if (!obj2 || Object.prototype.toString.call(obj2) !== '[object Object]') {
+		return obj1;
+	}
+
+	//
+	// Variables
+	//
+
+	var diffs = {};
+	var key;
+
+
+	//
+	// Methods
+	//
+
+	/**
+	 * Check if two arrays are equal
+	 * @param  {Array}   arr1 The first array
+	 * @param  {Array}   arr2 The second array
+	 * @return {Boolean}      If true, both arrays are equal
+	 */
+	var arraysMatch = function (arr1, arr2) {
+
+		// Check if the arrays are the same length
+		if (arr1.length !== arr2.length) return false;
+
+		// Check if all items exist and are in the same order
+		for (var i = 0; i < arr1.length; i++) {
+			if (arr1[i] !== arr2[i]) return false;
+		}
+
+		// Otherwise, return true
+		return true;
+
+	};
+
+	/**
+	 * Compare two items and push non-matches to object
+	 * @param  {*}      item1 The first item
+	 * @param  {*}      item2 The second item
+	 * @param  {String} key   The key in our object
+	 */
+	var compare = function (item1, item2, key) {
+
+		// Get the object type
+		var type1 = Object.prototype.toString.call(item1);
+		var type2 = Object.prototype.toString.call(item2);
+
+		// If type2 is undefined it has been removed
+		if (type2 === '[object Undefined]') {
+			diffs[key] = null;
+			return;
+		}
+
+		// If items are different types
+		if (type1 !== type2) {
+			diffs[key] = item2;
+			return;
+		}
+
+		// If an object, compare recursively
+		if (type1 === '[object Object]') {
+			var objDiff = diff(item1, item2);
+			if (Object.keys(objDiff).length > 1) {
+				diffs[key] = objDiff;
+			}
+			return;
+		}
+
+		// If an array, compare
+		if (type1 === '[object Array]') {
+			if (!arraysMatch(item1, item2)) {
+				diffs[key] = item2;
+			}
+			return;
+		}
+
+		// Else if it's a function, convert to a string and compare
+		// Otherwise, just compare
+		if (type1 === '[object Function]') {
+			if (item1.toString() !== item2.toString()) {
+				diffs[key] = item2;
+			}
+		} else {
+			if (item1 !== item2 ) {
+				diffs[key] = item2;
+			}
+		}
+
+	};
+
+
+	//
+	// Compare our objects
+	//
+
+	// Loop through the first object
+	for (key in obj1) {
+		if (obj1.hasOwnProperty(key)) {
+			compare(obj1[key], obj2[key], key);
+		}
+	}
+
+	// Loop through the second object and find missing items
+	for (key in obj2) {
+		if (obj2.hasOwnProperty(key)) {
+			if (!obj1[key] && obj1[key] !== obj2[key] ) {
+				diffs[key] = obj2[key];
+			}
+		}
+	}
+
+	// Return the object of differences
+	return diffs;
+
+}
 function organizrConsole(subject,msg,type = 'info'){
 
 	let color;

+ 1 - 2
plugins/bower_components/jsgrid/.gitignore

@@ -1,4 +1,3 @@
 .idea
 node_modules
-dist
-_site
+_site

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 168 - 0
plugins/bower_components/jsgrid/dist/jsgrid-theme.css


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 6 - 0
plugins/bower_components/jsgrid/dist/jsgrid-theme.min.css


+ 126 - 0
plugins/bower_components/jsgrid/dist/jsgrid.css

@@ -0,0 +1,126 @@
+/*
+ * jsGrid v1.4.1 (http://js-grid.com)
+ * (c) 2016 Artem Tabalin
+ * Licensed under MIT (https://github.com/tabalinas/jsgrid/blob/master/LICENSE)
+ */
+
+.jsgrid {
+    position: relative;
+    overflow: hidden;
+    font-size: 1em;
+}
+
+.jsgrid, .jsgrid *, .jsgrid *:before, .jsgrid *:after {
+    box-sizing: border-box;
+}
+
+.jsgrid input,
+.jsgrid textarea,
+.jsgrid select {
+    font-size: 1em;
+}
+
+.jsgrid-grid-header {
+    overflow-x: hidden;
+    overflow-y: scroll;
+    -webkit-user-select: none;
+    -khtml-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    -o-user-select: none;
+    user-select: none;
+}
+
+.jsgrid-grid-body {
+    overflow-x: auto;
+    overflow-y: scroll;
+    -webkit-overflow-scrolling: touch;
+}
+
+.jsgrid-table {
+    width: 100%;
+    table-layout: fixed;
+    border-collapse: collapse;
+    border-spacing: 0;
+}
+
+.jsgrid-table td {
+    padding: 0.5em 0.5em;
+}
+
+.jsgrid-table td,
+.jsgrid-table th {
+    box-sizing: border-box;
+}
+
+.jsgrid-align-left {
+    text-align: left;
+}
+
+.jsgrid-align-center,
+.jsgrid-align-center input,
+.jsgrid-align-center textarea,
+.jsgrid-align-center select {
+    text-align: center;
+}
+
+.jsgrid-align-right,
+.jsgrid-align-right input,
+.jsgrid-align-right textarea,
+.jsgrid-align-right select {
+    text-align: right;
+}
+
+.jsgrid-header-row > th {
+    padding: .5em .5em;
+}
+
+.jsgrid-filter-row input,
+.jsgrid-filter-row textarea,
+.jsgrid-filter-row select,
+.jsgrid-edit-row input,
+.jsgrid-edit-row textarea,
+.jsgrid-edit-row select,
+.jsgrid-insert-row input,
+.jsgrid-insert-row textarea,
+.jsgrid-insert-row select {
+    width: 100%;
+    padding: .3em .5em;
+}
+
+.jsgrid-filter-row input[type='checkbox'],
+.jsgrid-edit-row input[type='checkbox'],
+.jsgrid-insert-row input[type='checkbox'] {
+    width: auto;
+}
+
+
+.jsgrid-selected-row td {
+    cursor: pointer;
+}
+
+.jsgrid-nodata-row td {
+    padding: .5em 0;
+    text-align: center;
+}
+
+.jsgrid-header-sort {
+    cursor: pointer;
+}
+
+.jsgrid-pager {
+    padding: .5em 0;
+}
+
+.jsgrid-pager-nav-button {
+    padding: .2em .6em;
+}
+
+.jsgrid-pager-nav-inactive-button {
+    display: none;
+    pointer-events: none;
+}
+
+.jsgrid-pager-page {
+    padding: .2em .6em;
+}

+ 2454 - 0
plugins/bower_components/jsgrid/dist/jsgrid.js

@@ -0,0 +1,2454 @@
+/*
+ * jsGrid v1.4.1 (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 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",
+
+        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",
+
+        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"));
+        },
+
+        onRefreshing: $.noop,
+        onRefreshed: $.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();
+        },
+
+        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));
+        },
+
+        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 "noDataText":
+                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.headerRowRenderer());
+
+            var $result = $("<tr>").addClass(this.headerRowClass);
+
+            this._eachField(function(field, index) {
+                var $th = this._prepareCell("<th>", field, "headercss")
+                    .append(field.headerTemplate ? field.headerTemplate() : "")
+                    .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) {
+            return $(cell).css("width", field.width)
+                .addClass((cssprop && field[cssprop]) || field.css)
+                .addClass(field.align ? ("jsgrid-align-" + field.align) : "");
+        },
+
+        _createFilterRow: function() {
+            if($.isFunction(this.filterRowRenderer))
+                return $(this.filterRowRenderer());
+
+            var $result = $("<tr>").addClass(this.filterRowClass);
+
+            this._eachField(function(field) {
+                this._prepareCell("<td>", field, "filtercss")
+                    .append(field.filterTemplate ? field.filterTemplate() : "")
+                    .appendTo($result);
+            });
+
+            return $result;
+        },
+
+        _createInsertRow: function() {
+            if($.isFunction(this.insertRowRenderer))
+                return $(this.insertRowRenderer());
+
+            var $result = $("<tr>").addClass(this.insertRowClass);
+
+            this._eachField(function(field) {
+                this._prepareCell("<td>", field, "insertcss")
+                    .append(field.insertTemplate ? field.insertTemplate() : "")
+                    .appendTo($result);
+            });
+
+            return $result;
+        },
+
+        _callEventHandler: function(handler, eventParams) {
+            handler.call(this, $.extend(eventParams, {
+                grid: this
+            }));
+
+            return eventParams;
+        },
+
+        reset: function() {
+            this._resetSorting();
+            this._resetPager();
+            this.refresh();
+        },
+
+        _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 noDataContent = getOrApply(this.noDataContent, this);
+
+            var amountOfFields = 0;
+            this._eachField(function() {
+                amountOfFields++;
+            });
+
+            return $("<tr>").addClass(this.noDataRowClass)
+                .append($("<td>").attr("colspan", amountOfFields).append(noDataContent));
+        },
+
+        _createNoDataContent: function() {
+            return $.isFunction(this.noDataRenderer)
+                ? this.noDataRenderer()
+                : this.noDataText;
+        },
+
+        _createRow: function(item, itemIndex) {
+            var $result;
+
+            if($.isFunction(this.rowRenderer)) {
+                $result = $(this.rowRenderer(item, 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);
+
+            if($.isFunction(field.cellRenderer)) {
+                $result = $(field.cellRenderer(fieldValue, item));
+            } else {
+                $result = $("<td>").append(field.itemTemplate ? field.itemTemplate(fieldValue, item) : fieldValue);
+            }
+
+            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 = $.inArray(this._sortField, $.grep(this.fields, function(f) { return f.visible; }));
+
+            this._headerRow.find("th").eq(fieldIndex)
+                .addClass(this._sortOrder === SORT_ORDER_ASC ? this.sortAscClass : this.sortDescClass);
+        },
+
+        _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 $headerGrid = this._headerGrid,
+                $bodyGrid = this._bodyGrid,
+                width = this.width;
+
+            if(width === "auto") {
+                $headerGrid.width("auto");
+                width = $headerGrid.outerWidth();
+            }
+
+            $headerGrid.width("");
+            $bodyGrid.width("");
+            this._container.width(width);
+            width = $headerGrid.outerWidth();
+            $bodyGrid.width(width);
+        },
+
+        _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;
+            }
+        },
+
+        _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 $.when(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, index) {
+                if(!field.validate)
+                    return;
+
+                var errors = this._validation.validate($.extend({
+                    value: this._getItemFieldValue(item, field),
+                    rules: field.validate
+                }, args));
+
+                this._setCellValidity($row.children().eq(index), 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.editRowRenderer(item, 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(field.editTemplate ? field.editTemplate(fieldValue, 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),
+                previousItem = $.extend(true, {}, updatingItem);
+
+            $.extend(true, updatingItem, editedItem);
+
+            var args = this._callEventHandler(this.onItemUpdating, {
+                row: $updatingRow,
+                item: updatingItem,
+                itemIndex: updatingItemIndex,
+                previousItem: previousItem
+            });
+
+            return this._controllerCall("updateItem", updatingItem, args.cancel, function(updatedItem) {
+                updatedItem = updatedItem || updatingItem;
+                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.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
+    };
+
+}(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();
+        },
+
+        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")
+            };
+        },
+
+        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 long",
+            validator: function(value, _, param) {
+                return value.length >= param;
+            }
+        },
+
+        maxLength: {
+            message: "Field value is too short",
+            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 large",
+            validator: function(value, _, param) {
+                return value >= param;
+            }
+        },
+
+        max: {
+            message: "Field value is too small",
+            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(value);
+
+            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 parseInt(this.filterControl.val() || 0, 10);
+        },
+
+        insertValue: function() {
+            return parseInt(this.insertControl.val() || 0, 10);
+        },
+
+        editValue: function() {
+            return parseInt(this.editControl.val() || 0, 10);
+        },
+
+        _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(value);
+
+            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;
+
+    function SelectField(config) {
+        this.items = [];
+        this.selectedIndex = -1;
+        this.valueField = "";
+        this.textField = "";
+
+        if(config.valueField && config.items.length) {
+            this.valueType = typeof config.items[0][config.valueField];
+        }
+
+        this.sorter = this.valueType;
+
+        NumberField.call(this, config);
+    }
+
+    SelectField.prototype = new NumberField({
+
+        align: "center",
+        valueType: "number",
+
+        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(value);
+
+            var $result = this.editControl = this._createSelect();
+            (value !== undefined) && $result.val(value);
+            return $result;
+        },
+
+        filterValue: function() {
+            var val = this.filterControl.val();
+            return this.valueType === "number" ? parseInt(val || 0, 10) : val;
+        },
+
+        insertValue: function() {
+            var val = this.insertControl.val();
+            return this.valueType === "number" ? parseInt(val || 0, 10) : val;
+        },
+
+        editValue: function() {
+            var val = this.editControl.val();
+            return this.valueType === "number" ? 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(value);
+
+            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));

+ 7 - 0
plugins/bower_components/jsgrid/dist/jsgrid.min.css

@@ -0,0 +1,7 @@
+/*
+ * jsGrid v1.4.1 (http://js-grid.com)
+ * (c) 2016 Artem Tabalin
+ * Licensed under MIT (https://github.com/tabalinas/jsgrid/blob/master/LICENSE)
+ */
+
+.jsgrid{position:relative;overflow:hidden;font-size:1em}.jsgrid,.jsgrid *,.jsgrid :after,.jsgrid :before{box-sizing:border-box}.jsgrid input,.jsgrid select,.jsgrid textarea{font-size:1em}.jsgrid-grid-header{overflow-x:hidden;overflow-y:scroll;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.jsgrid-grid-body{overflow-x:auto;overflow-y:scroll;-webkit-overflow-scrolling:touch}.jsgrid-table{width:100%;table-layout:fixed;border-collapse:collapse;border-spacing:0}.jsgrid-table td{padding:.5em}.jsgrid-table td,.jsgrid-table th{box-sizing:border-box}.jsgrid-align-left{text-align:left}.jsgrid-align-center,.jsgrid-align-center input,.jsgrid-align-center select,.jsgrid-align-center textarea{text-align:center}.jsgrid-align-right,.jsgrid-align-right input,.jsgrid-align-right select,.jsgrid-align-right textarea{text-align:right}.jsgrid-header-row>th{padding:.5em}.jsgrid-edit-row input,.jsgrid-edit-row select,.jsgrid-edit-row textarea,.jsgrid-filter-row input,.jsgrid-filter-row select,.jsgrid-filter-row textarea,.jsgrid-insert-row input,.jsgrid-insert-row select,.jsgrid-insert-row textarea{width:100%;padding:.3em .5em}.jsgrid-edit-row input[type=checkbox],.jsgrid-filter-row input[type=checkbox],.jsgrid-insert-row input[type=checkbox]{width:auto}.jsgrid-selected-row td{cursor:pointer}.jsgrid-nodata-row td{padding:.5em 0;text-align:center}.jsgrid-header-sort{cursor:pointer}.jsgrid-pager{padding:.5em 0}.jsgrid-pager-nav-button{padding:.2em .6em}.jsgrid-pager-nav-inactive-button{display:none;pointer-events:none}.jsgrid-pager-page{padding:.2em .6em}

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 6 - 0
plugins/bower_components/jsgrid/dist/jsgrid.min.js


Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů