steve 8 лет назад
Родитель
Сommit
fd175565bb
73 измененных файлов с 26504 добавлено и 0 удалено
  1. 21 0
      src/api/static/css/default.css
  2. 11 0
      src/api/static/index.html
  3. 9472 0
      src/api/static/js/jquery/jquery.min.js
  4. 20 0
      src/api/static/js/jsgrid/.editorconfig
  5. 4 0
      src/api/static/js/jsgrid/.gitignore
  6. 5 0
      src/api/static/js/jsgrid/.npmignore
  7. 7 0
      src/api/static/js/jsgrid/.travis.yml
  8. 130 0
      src/api/static/js/jsgrid/Gruntfile.js
  9. 22 0
      src/api/static/js/jsgrid/LICENSE
  10. 2169 0
      src/api/static/js/jsgrid/README.md
  11. 28 0
      src/api/static/js/jsgrid/bower.json
  12. BIN
      src/api/static/js/jsgrid/css/icons-2x.png
  13. BIN
      src/api/static/js/jsgrid/css/icons.png
  14. 120 0
      src/api/static/js/jsgrid/css/jsgrid.css
  15. 252 0
      src/api/static/js/jsgrid/css/theme.css
  16. 59 0
      src/api/static/js/jsgrid/demos/basic.html
  17. 102 0
      src/api/static/js/jsgrid/demos/batch-delete.html
  18. 97 0
      src/api/static/js/jsgrid/demos/custom-grid-field.html
  19. 90 0
      src/api/static/js/jsgrid/demos/custom-load-indicator.html
  20. 79 0
      src/api/static/js/jsgrid/demos/custom-row-renderer.html
  21. 85 0
      src/api/static/js/jsgrid/demos/custom-view.html
  22. 212 0
      src/api/static/js/jsgrid/demos/data-manipulation.html
  23. 884 0
      src/api/static/js/jsgrid/demos/db.js
  24. 82 0
      src/api/static/js/jsgrid/demos/demos.css
  25. 72 0
      src/api/static/js/jsgrid/demos/external-pager.html
  26. 31 0
      src/api/static/js/jsgrid/demos/index.html
  27. 90 0
      src/api/static/js/jsgrid/demos/loading-by-page.html
  28. 62 0
      src/api/static/js/jsgrid/demos/localization.html
  29. 74 0
      src/api/static/js/jsgrid/demos/odata-service.html
  30. 83 0
      src/api/static/js/jsgrid/demos/rows-reordering.html
  31. 78 0
      src/api/static/js/jsgrid/demos/sorting.html
  32. 50 0
      src/api/static/js/jsgrid/demos/static-data.html
  33. 61 0
      src/api/static/js/jsgrid/demos/validation.html
  34. 235 0
      src/api/static/js/jsgrid/external/qunit/qunit-1.10.0.css
  35. 1977 0
      src/api/static/js/jsgrid/external/qunit/qunit-1.10.0.js
  36. 258 0
      src/api/static/js/jsgrid/jsgrid-theme.css
  37. 7 0
      src/api/static/js/jsgrid/jsgrid-theme.min.css
  38. 126 0
      src/api/static/js/jsgrid/jsgrid.css
  39. 2516 0
      src/api/static/js/jsgrid/jsgrid.js
  40. 7 0
      src/api/static/js/jsgrid/jsgrid.min.css
  41. 8 0
      src/api/static/js/jsgrid/jsgrid.min.js
  42. 38 0
      src/api/static/js/jsgrid/package.json
  43. 97 0
      src/api/static/js/jsgrid/src/fields/jsgrid.field.checkbox.js
  44. 223 0
      src/api/static/js/jsgrid/src/fields/jsgrid.field.control.js
  45. 41 0
      src/api/static/js/jsgrid/src/fields/jsgrid.field.number.js
  46. 121 0
      src/api/static/js/jsgrid/src/fields/jsgrid.field.select.js
  47. 69 0
      src/api/static/js/jsgrid/src/fields/jsgrid.field.text.js
  48. 34 0
      src/api/static/js/jsgrid/src/fields/jsgrid.field.textarea.js
  49. 46 0
      src/api/static/js/jsgrid/src/i18n/de.js
  50. 46 0
      src/api/static/js/jsgrid/src/i18n/es.js
  51. 47 0
      src/api/static/js/jsgrid/src/i18n/fr.js
  52. 46 0
      src/api/static/js/jsgrid/src/i18n/he.js
  53. 46 0
      src/api/static/js/jsgrid/src/i18n/ja.js
  54. 46 0
      src/api/static/js/jsgrid/src/i18n/ka.js
  55. 62 0
      src/api/static/js/jsgrid/src/i18n/pl.js
  56. 46 0
      src/api/static/js/jsgrid/src/i18n/pt-br.js
  57. 46 0
      src/api/static/js/jsgrid/src/i18n/pt.js
  58. 47 0
      src/api/static/js/jsgrid/src/i18n/ru.js
  59. 47 0
      src/api/static/js/jsgrid/src/i18n/tr.js
  60. 46 0
      src/api/static/js/jsgrid/src/i18n/zh-cn.js
  61. 46 0
      src/api/static/js/jsgrid/src/i18n/zh-tw.js
  62. 1467 0
      src/api/static/js/jsgrid/src/jsgrid.core.js
  63. 72 0
      src/api/static/js/jsgrid/src/jsgrid.field.js
  64. 82 0
      src/api/static/js/jsgrid/src/jsgrid.load-indicator.js
  65. 122 0
      src/api/static/js/jsgrid/src/jsgrid.load-strategies.js
  66. 36 0
      src/api/static/js/jsgrid/src/jsgrid.sort-strategies.js
  67. 135 0
      src/api/static/js/jsgrid/src/jsgrid.validation.js
  68. 37 0
      src/api/static/js/jsgrid/tests/index.html
  69. 461 0
      src/api/static/js/jsgrid/tests/jsgrid.field.tests.js
  70. 51 0
      src/api/static/js/jsgrid/tests/jsgrid.sort-strategies.tests.js
  71. 2821 0
      src/api/static/js/jsgrid/tests/jsgrid.tests.js
  72. 279 0
      src/api/static/js/jsgrid/tests/jsgrid.validation.tests.js
  73. 15 0
      src/api/templates/dashboard.html

+ 21 - 0
src/api/static/css/default.css

@@ -0,0 +1,21 @@
+body {
+	font-size:14px;
+	font-family:sans-serif;
+	font-weight:lighter;
+}
+.small {
+	font-family:verdana;
+	font-size:12px;
+	color: gray;
+	font-weight:lighter;
+}
+
+.left {float:left}
+.right{float:right}
+.caption {
+	font-size:22px;
+	margin:4px;
+	padding:4px;
+	height:24px;
+	font-family:sans-serif;
+}

+ 11 - 0
src/api/static/index.html

@@ -0,0 +1,11 @@
+<link type="text/css" rel="stylesheet" href="{{ context }}/js/jsgrid/jsgrid.min.css" />
+<link type="text/css" rel="stylesheet" href="{{ context }}/js/jsgrid/jsgrid-theme.min.css" />
+<script src="{{ context }}/static/js/jsgrid.js"></script>
+<script src="{{ context }}/static/js/jquery/jquery.min.js"></script>
+
+<title></title>
+<body>
+<div class="caption">
+<div class="left">Process Monitoring</div>
+</div>
+</body>

Разница между файлами не показана из-за своего большого размера
+ 9472 - 0
src/api/static/js/jquery/jquery.min.js


+ 20 - 0
src/api/static/js/jsgrid/.editorconfig

@@ -0,0 +1,20 @@
+# EditorConfig helps developers define and maintain consistent
+# coding styles between different editors and IDEs
+# editorconfig.org
+
+root = true
+
+[*]
+
+# Change these settings to your own preference
+indent_style = space
+indent_size = 4
+
+# We recommend you to keep these unchanged
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false

+ 4 - 0
src/api/static/js/jsgrid/.gitignore

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

+ 5 - 0
src/api/static/js/jsgrid/.npmignore

@@ -0,0 +1,5 @@
+.idea
+node_modules
+_site
+.editorconfig
+.travis.yml

+ 7 - 0
src/api/static/js/jsgrid/.travis.yml

@@ -0,0 +1,7 @@
+language: node_js
+node_js:
+  - '0.10'
+before_script:
+  - 'npm install -g grunt-cli'
+
+script: grunt test --verbose --force

+ 130 - 0
src/api/static/js/jsgrid/Gruntfile.js

@@ -0,0 +1,130 @@
+module.exports = function(grunt) {
+    "use strict"
+
+    var banner =
+        "/*\n" +
+        " * jsGrid v<%= pkg.version %> (<%= pkg.homepage %>)\n" +
+        " * (c) <%= grunt.template.today('yyyy') %> <%= pkg.author %>\n" +
+        " * Licensed under <%= pkg.license.type %> (<%= pkg.license.url %>)\n" +
+        " */\n";
+
+    grunt.initConfig({
+        pkg: grunt.file.readJSON("package.json"),
+
+        copy: {
+            imgs: {
+                expand: true,
+                cwd: "css/",
+                src: "*.png",
+                dest: "dist/"
+            },
+            i18n: {
+                expand: true,
+                cwd: "src/i18n/",
+                src: "*.js",
+                dest: "dist/i18n/",
+                rename: function(dest, src) {
+                    return dest + "jsgrid-" + src;
+                }
+            }
+        },
+
+        concat: {
+            options: {
+                banner: banner + "\n",
+                separator: "\n"
+            },
+            js: {
+                src: [
+                    "src/jsgrid.core.js",
+                    "src/jsgrid.load-indicator.js",
+                    "src/jsgrid.load-strategies.js",
+                    "src/jsgrid.sort-strategies.js",
+                    "src/jsgrid.validation.js",
+                    "src/jsgrid.field.js",
+                    "src/fields/jsgrid.field.text.js",
+                    "src/fields/jsgrid.field.number.js",
+                    "src/fields/jsgrid.field.textarea.js",
+                    "src/fields/jsgrid.field.select.js",
+                    "src/fields/jsgrid.field.checkbox.js",
+                    "src/fields/jsgrid.field.control.js"
+                ],
+                dest: "dist/<%= pkg.name %>.js"
+            },
+            css: {
+                src: "css/jsgrid.css",
+                dest: "dist/<%= pkg.name %>.css"
+            },
+            theme: {
+                src: "css/theme.css",
+                dest: "dist/<%= pkg.name %>-theme.css"
+            }
+        },
+
+        "string-replace": {
+            version: {
+                files: [{
+                    src: "<%= concat.js.dest %>",
+                    dest: "<%= concat.js.dest %>"
+                }],
+                options: {
+                    replacements: [{
+                        pattern: /"@VERSION"/g,
+                        replacement: "'<%= pkg.version %>'"
+                    }]
+                }
+            }
+        },
+
+        imageEmbed: {
+            options: {
+                deleteAfterEncoding : true
+            },
+            theme: {
+                src: "<%= concat.theme.dest %>",
+                dest: "<%= concat.theme.dest %>"
+            }
+        },
+
+        uglify: {
+            options : {
+                banner: banner + "\n"
+            },
+            js: {
+                src: "<%= concat.js.dest %>",
+                dest: "dist/<%= pkg.name %>.min.js"
+            }
+        },
+
+        cssmin: {
+            options : {
+                banner: banner
+            },
+            css: {
+                src: "<%= concat.css.dest %>",
+                dest: "dist/<%= pkg.name %>.min.css"
+            },
+            theme: {
+                src: "<%= concat.theme.dest %>",
+                dest: "dist/<%= pkg.name %>-theme.min.css"
+            }
+        },
+
+        qunit: {
+            files: ["tests/index.html"]
+        }
+
+    });
+
+    grunt.loadNpmTasks("grunt-contrib-copy");
+    grunt.loadNpmTasks("grunt-contrib-concat");
+    grunt.loadNpmTasks("grunt-contrib-uglify");
+    grunt.loadNpmTasks("grunt-image-embed");
+    grunt.loadNpmTasks("grunt-contrib-cssmin");
+    grunt.loadNpmTasks("grunt-contrib-qunit");
+    grunt.loadNpmTasks('grunt-string-replace');
+
+    grunt.registerTask("default", ["copy", "concat", "string-replace", "imageEmbed", "uglify", "cssmin"]);
+
+    grunt.registerTask("test", "qunit");
+};

+ 22 - 0
src/api/static/js/jsgrid/LICENSE

@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Artem Tabalin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+

Разница между файлами не показана из-за своего большого размера
+ 2169 - 0
src/api/static/js/jsgrid/README.md


+ 28 - 0
src/api/static/js/jsgrid/bower.json

@@ -0,0 +1,28 @@
+{
+    "name": "jsgrid",
+    "version": "1.5.3",
+    "main": [
+        "dist/jsgrid.js",
+        "dist/jsgrid.css",
+        "dist/jsgrid-theme.css"
+    ],
+    "ignore": [
+        "demos",
+        "external",
+        "tests",
+        ".editorconfig",
+        ".gitignore",
+        ".travis.yml",
+        "bower.json",
+        "Gruntfile.js",
+        "LICENSE",
+        "package.json",
+        "README.md"
+    ],
+    "dependencies": {
+        "jquery": ">=1.8.3"
+    },
+    "devDependencies": {
+        "qunit": ">=1.10.0"
+    }
+}

BIN
src/api/static/js/jsgrid/css/icons-2x.png


BIN
src/api/static/js/jsgrid/css/icons.png


+ 120 - 0
src/api/static/js/jsgrid/css/jsgrid.css

@@ -0,0 +1,120 @@
+.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-cell {
+    padding: 0.5em 0.5em;
+}
+
+.jsgrid-сell,
+.jsgrid-header-cell {
+    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-cell {
+    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 .jsgrid-cell {
+    cursor: pointer;
+}
+
+.jsgrid-nodata-row .jsgrid-cell {
+    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;
+}

+ 252 - 0
src/api/static/js/jsgrid/css/theme.css

@@ -0,0 +1,252 @@
+.jsgrid-grid-header,
+.jsgrid-grid-body,
+.jsgrid-header-row > .jsgrid-header-cell,
+.jsgrid-filter-row > .jsgrid-cell,
+.jsgrid-insert-row > .jsgrid-cell,
+.jsgrid-edit-row > .jsgrid-cell {
+    border: 1px solid #e9e9e9;
+}
+
+.jsgrid-header-row > .jsgrid-header-cell {
+    border-top: 0;
+}
+
+.jsgrid-header-row > .jsgrid-header-cell,
+.jsgrid-filter-row > .jsgrid-cell,
+.jsgrid-insert-row > .jsgrid-cell {
+    border-bottom: 0;
+}
+
+.jsgrid-header-row > .jsgrid-header-cell:first-child,
+.jsgrid-filter-row > .jsgrid-cell:first-child,
+.jsgrid-insert-row > .jsgrid-cell:first-child {
+    border-left: none;
+}
+
+.jsgrid-header-row > .jsgrid-header-cell:last-child,
+.jsgrid-filter-row > .jsgrid-cell:last-child,
+.jsgrid-insert-row > .jsgrid-cell:last-child {
+    border-right: none;
+}
+
+.jsgrid-header-row .jsgrid-align-right,
+.jsgrid-header-row .jsgrid-align-left {
+    text-align: center;
+}
+
+.jsgrid-grid-header {
+    background: #f9f9f9;
+}
+
+.jsgrid-header-scrollbar {
+    scrollbar-arrow-color: #f1f1f1;
+    scrollbar-base-color: #f1f1f1;
+    scrollbar-3dlight-color: #f1f1f1;
+    scrollbar-highlight-color: #f1f1f1;
+    scrollbar-track-color: #f1f1f1;
+    scrollbar-shadow-color: #f1f1f1;
+    scrollbar-dark-shadow-color: #f1f1f1;
+}
+
+.jsgrid-header-scrollbar::-webkit-scrollbar {
+    visibility: hidden;
+}
+
+.jsgrid-header-scrollbar::-webkit-scrollbar-track {
+    background: #f1f1f1;
+}
+
+.jsgrid-header-sortable:hover {
+    cursor: pointer;
+    background: #fcfcfc;
+}
+
+.jsgrid-header-row .jsgrid-header-sort {
+    background: #c4e2ff;
+}
+
+.jsgrid-header-sort:before {
+    content: " ";
+    display: block;
+    float: left;
+    width: 0;
+    height: 0;
+    border-style: solid;
+}
+
+.jsgrid-header-sort-asc:before {
+    border-width: 0 5px 5px 5px;
+    border-color: transparent transparent #009a67 transparent;
+}
+
+.jsgrid-header-sort-desc:before {
+    border-width: 5px 5px 0 5px;
+    border-color: #009a67 transparent transparent transparent;
+}
+
+.jsgrid-grid-body {
+    border-top: none;
+}
+
+.jsgrid-cell {
+    border: #f3f3f3 1px solid;
+}
+
+.jsgrid-grid-body .jsgrid-row:first-child .jsgrid-cell,
+.jsgrid-grid-body .jsgrid-alt-row:first-child .jsgrid-cell {
+    border-top: none;
+}
+
+.jsgrid-grid-body .jsgrid-cell:first-child {
+    border-left: none;
+}
+
+.jsgrid-grid-body .jsgrid-cell:last-child {
+    border-right: none;
+}
+
+.jsgrid-row > .jsgrid-cell {
+    background: #fff;
+}
+
+.jsgrid-alt-row > .jsgrid-cell {
+    background: #fcfcfc;
+}
+
+.jsgrid-header-row > .jsgrid-header-cell {
+    background: #f9f9f9;
+}
+
+.jsgrid-filter-row > .jsgrid-cell {
+    background: #fcfcfc;
+}
+
+.jsgrid-insert-row > .jsgrid-cell {
+    background: #e3ffe5;
+}
+
+.jsgrid-edit-row > .jsgrid-cell {
+    background: #fdffe3;
+}
+
+.jsgrid-selected-row > .jsgrid-cell {
+    background: #c4e2ff;
+    border-color: #c4e2ff;
+}
+
+.jsgrid-nodata-row > .jsgrid-cell {
+    background: #fff;
+}
+
+.jsgrid-invalid input,
+.jsgrid-invalid select,
+.jsgrid-invalid textarea {
+    background: #ffe3e5;
+    border: 1px solid #ff808a;
+}
+
+.jsgrid-pager-current-page {
+    font-weight: bold;
+}
+
+.jsgrid-pager-nav-inactive-button a {
+    color: #d3d3d3;
+}
+
+.jsgrid-button + .jsgrid-button {
+    margin-left: 5px;
+}
+
+.jsgrid-button:hover {
+    opacity: .5;
+    transition: opacity 200ms linear;
+}
+
+.jsgrid .jsgrid-button {
+    width: 16px;
+    height: 16px;
+    border: none;
+    cursor: pointer;
+    background-image: url(icons.png);
+    background-repeat: no-repeat;
+    background-color: transparent;
+}
+
+@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2) {
+    .jsgrid .jsgrid-button {
+        background-image: url(icons-2x.png);
+        background-size: 24px 352px;
+    }
+}
+
+.jsgrid .jsgrid-mode-button {
+    width: 24px;
+    height: 24px;
+}
+
+.jsgrid-mode-on-button {
+    opacity: .5;
+}
+
+.jsgrid-cancel-edit-button { background-position: 0 0; width: 16px; height: 16px; }
+.jsgrid-clear-filter-button { background-position: 0 -40px; width: 16px; height: 16px; }
+.jsgrid-delete-button { background-position: 0 -80px; width: 16px; height: 16px; }
+.jsgrid-edit-button { background-position: 0 -120px; width: 16px; height: 16px; }
+.jsgrid-insert-mode-button { background-position: 0 -160px; width: 24px; height: 24px; }
+.jsgrid-insert-button { background-position: 0 -208px; width: 16px; height: 16px; }
+.jsgrid-search-mode-button { background-position: 0 -248px; width: 24px; height: 24px; }
+.jsgrid-search-button { background-position: 0 -296px; width: 16px; height: 16px; }
+.jsgrid-update-button { background-position: 0 -336px; width: 16px; height: 16px; }
+
+
+.jsgrid-load-shader {
+    background: #ddd;
+    opacity: .5;
+    filter: alpha(opacity=50);
+}
+
+.jsgrid-load-panel {
+    width: 15em;
+    height: 5em;
+    background: #fff;
+    border: 1px solid #e9e9e9;
+    padding-top: 3em;
+    text-align: center;
+}
+
+.jsgrid-load-panel:before {
+    content: ' ';
+    position: absolute;
+    top: .5em;
+    left: 50%;
+    margin-left: -1em;
+    width: 2em;
+    height: 2em;
+    border: 2px solid #009a67;
+    border-right-color: transparent;
+    border-radius: 50%;
+    -webkit-animation: indicator 1s linear infinite;
+    animation: indicator 1s linear infinite;
+}
+
+@-webkit-keyframes indicator
+{
+    from { -webkit-transform: rotate(0deg); }
+    50%  { -webkit-transform: rotate(180deg); }
+    to   { -webkit-transform: rotate(360deg); }
+}
+
+@keyframes indicator
+{
+    from { transform: rotate(0deg); }
+    50%  { transform: rotate(180deg); }
+    to   { transform: rotate(360deg); }
+}
+
+/* old IE */
+.jsgrid-load-panel {
+    padding-top: 1.5em\9;
+}
+.jsgrid-load-panel:before {
+    display: none\9;
+}

+ 59 - 0
src/api/static/js/jsgrid/demos/basic.html

@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <title>jsGrid - Basic Scenario</title>
+    <link rel="stylesheet" type="text/css" href="demos.css" />
+    <link href='http://fonts.googleapis.com/css?family=Open+Sans:300,600,400' rel='stylesheet' type='text/css'>
+
+    <link rel="stylesheet" type="text/css" href="../css/jsgrid.css" />
+    <link rel="stylesheet" type="text/css" href="../css/theme.css" />
+
+    <script src="../external/jquery/jquery-1.8.3.js"></script>
+    <script src="db.js"></script>
+
+    <script src="../src/jsgrid.core.js"></script>
+    <script src="../src/jsgrid.load-indicator.js"></script>
+    <script src="../src/jsgrid.load-strategies.js"></script>
+    <script src="../src/jsgrid.sort-strategies.js"></script>
+    <script src="../src/jsgrid.field.js"></script>
+    <script src="../src/fields/jsgrid.field.text.js"></script>
+    <script src="../src/fields/jsgrid.field.number.js"></script>
+    <script src="../src/fields/jsgrid.field.select.js"></script>
+    <script src="../src/fields/jsgrid.field.checkbox.js"></script>
+    <script src="../src/fields/jsgrid.field.control.js"></script>
+</head>
+<body>
+    <h1>Basic Scenario</h1>
+    <div id="jsGrid"></div>
+
+    <script>
+        $(function() {
+
+            $("#jsGrid").jsGrid({
+                height: "70%",
+                width: "100%",
+                filtering: true,
+                editing: true,
+                inserting: true,
+                sorting: true,
+                paging: true,
+                autoload: true,
+                pageSize: 15,
+                pageButtonCount: 5,
+                deleteConfirm: "Do you really want to delete the client?",
+                controller: db,
+                fields: [
+                    { name: "Name", type: "text", width: 150 },
+                    { name: "Age", type: "number", width: 50 },
+                    { name: "Address", type: "text", width: 200 },
+                    { name: "Country", type: "select", items: db.countries, valueField: "Id", textField: "Name" },
+                    { name: "Married", type: "checkbox", title: "Is Married", sorting: false },
+                    { type: "control" }
+                ]
+            });
+
+        });
+    </script>
+</body>
+</html>

+ 102 - 0
src/api/static/js/jsgrid/demos/batch-delete.html

@@ -0,0 +1,102 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <title>jsGrid - Batch Delete</title>
+    <link rel="stylesheet" type="text/css" href="demos.css" />
+    <link href='http://fonts.googleapis.com/css?family=Open+Sans:300,600,400' rel='stylesheet' type='text/css'>
+
+    <link rel="stylesheet" type="text/css" href="../css/jsgrid.css" />
+    <link rel="stylesheet" type="text/css" href="../css/theme.css" />
+
+    <script src="../external/jquery/jquery-1.8.3.js"></script>
+    <script src="db.js"></script>
+
+    <script src="../src/jsgrid.core.js"></script>
+    <script src="../src/jsgrid.load-indicator.js"></script>
+    <script src="../src/jsgrid.load-strategies.js"></script>
+    <script src="../src/jsgrid.sort-strategies.js"></script>
+    <script src="../src/jsgrid.field.js"></script>
+    <script src="../src/fields/jsgrid.field.text.js"></script>
+    <script src="../src/fields/jsgrid.field.number.js"></script>
+    <script src="../src/fields/jsgrid.field.control.js"></script>
+</head>
+<body>
+    <h1>Batch Delete</h1>
+
+    <div id="jsGrid"></div>
+
+    <script>
+        $(function() {
+
+            $("#jsGrid").jsGrid({
+                height: "50%",
+                width: "100%",
+                autoload: true,
+                confirmDeleting: false,
+                paging: true,
+                controller: {
+                    loadData: function() {
+                        return db.clients;
+                    }
+                },
+                fields: [
+                    {
+                        headerTemplate: function() {
+                            return $("<button>").attr("type", "button").text("Delete")
+                                    .on("click", function () {
+                                        deleteSelectedItems();
+                                    });
+                        },
+                        itemTemplate: function(_, item) {
+                            return $("<input>").attr("type", "checkbox")
+                                    .prop("checked", $.inArray(item, selectedItems) > -1)
+                                    .on("change", function () {
+                                        $(this).is(":checked") ? selectItem(item) : unselectItem(item);
+                                    });
+                        },
+                        align: "center",
+                        width: 50
+                    },
+                    { name: "Name", type: "text", width: 150 },
+                    { name: "Age", type: "number", width: 50 },
+                    { name: "Address", type: "text", width: 200 }
+                ]
+            });
+
+
+            var selectedItems = [];
+
+            var selectItem = function(item) {
+                selectedItems.push(item);
+            };
+
+            var unselectItem = function(item) {
+                selectedItems = $.grep(selectedItems, function(i) {
+                    return i !== item;
+                });
+            };
+
+            var deleteSelectedItems = function() {
+                if(!selectedItems.length || !confirm("Are you sure?"))
+                    return;
+
+                deleteClientsFromDb(selectedItems);
+
+                var $grid = $("#jsGrid");
+                $grid.jsGrid("option", "pageIndex", 1);
+                $grid.jsGrid("loadData");
+
+                selectedItems = [];
+            };
+
+            var deleteClientsFromDb = function(deletingClients) {
+                db.clients = $.map(db.clients, function(client) {
+                    return ($.inArray(client, deletingClients) > -1) ? null : client;
+                });
+            };
+
+        });
+    </script>
+</body>
+</html>

+ 97 - 0
src/api/static/js/jsgrid/demos/custom-grid-field.html

@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <title>jsGrid - Custom Grid Field Scenario</title>
+    <link rel="stylesheet" type="text/css" href="demos.css" />
+    <link href='http://fonts.googleapis.com/css?family=Open+Sans:300,600,400' rel='stylesheet' type='text/css' />
+
+    <link rel="stylesheet" href="http://code.jquery.com/ui/1.11.2/themes/cupertino/jquery-ui.css">
+    <script src="http://code.jquery.com/jquery-1.10.2.js"></script>
+    <script src="http://code.jquery.com/ui/1.11.2/jquery-ui.js"></script>
+
+    <link rel="stylesheet" type="text/css" href="../css/jsgrid.css" />
+    <link rel="stylesheet" type="text/css" href="../css/theme.css" />
+
+    <script src="db.js"></script>
+
+    <script src="../src/jsgrid.core.js"></script>
+    <script src="../src/jsgrid.load-indicator.js"></script>
+    <script src="../src/jsgrid.load-strategies.js"></script>
+    <script src="../src/jsgrid.sort-strategies.js"></script>
+    <script src="../src/jsgrid.field.js"></script>
+    <script src="../src/fields/jsgrid.field.text.js"></script>
+    <script src="../src/fields/jsgrid.field.control.js"></script>
+
+    <style>
+        .hasDatepicker {
+            width: 100px;
+            text-align: center;
+        }
+
+        .ui-datepicker * {
+            font-family: 'Helvetica Neue Light', 'Open Sans', Helvetica;
+            font-size: 14px;
+            font-weight: 300 !important;
+        }
+    </style>
+</head>
+<body>
+    <h1>Custom Grid DateField</h1>
+    <div id="jsGrid"></div>
+
+    <script>
+        $(function() {
+
+            var MyDateField = function(config) {
+                jsGrid.Field.call(this, config);
+            };
+
+            MyDateField.prototype = new jsGrid.Field({
+                sorter: function(date1, date2) {
+                    return new Date(date1) - new Date(date2);
+                },
+
+                itemTemplate: function(value) {
+                    return new Date(value).toDateString();
+                },
+
+                insertTemplate: function(value) {
+                    return this._insertPicker = $("<input>").datepicker({ defaultDate: new Date() });
+                },
+
+                editTemplate: function(value) {
+                    return this._editPicker = $("<input>").datepicker().datepicker("setDate", new Date(value));
+                },
+
+                insertValue: function() {
+                    return this._insertPicker.datepicker("getDate").toISOString();
+                },
+
+                editValue: function() {
+                    return this._editPicker.datepicker("getDate").toISOString();
+                }
+            });
+
+            jsGrid.fields.myDateField = MyDateField;
+
+            $("#jsGrid").jsGrid({
+                height: "70%",
+                width: "100%",
+                inserting: true,
+                editing: true,
+                sorting: true,
+                paging: true,
+                fields: [
+                    { name: "Account", width: 150, align: "center" },
+                    { name: "Name", type: "text" },
+                    { name: "RegisterDate", type: "myDateField", width: 100, align: "center" },
+                    { type: "control", editButton: false, modeSwitchButton: false }
+                ],
+                data: db.users
+            });
+
+        });
+    </script>
+</body>
+</html>

+ 90 - 0
src/api/static/js/jsgrid/demos/custom-load-indicator.html

@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <title>jsGrid - Custom Load Indicator</title>
+    <link rel="stylesheet" type="text/css" href="demos.css" />
+    <link href='http://fonts.googleapis.com/css?family=Open+Sans:300,600,400' rel='stylesheet' type='text/css'>
+
+    <link rel="stylesheet" type="text/css" href="../css/jsgrid.css" />
+    <link rel="stylesheet" type="text/css" href="../css/theme.css" />
+
+    <script src="http://cdnjs.cloudflare.com/ajax/libs/spin.js/2.3.1/spin.min.js"></script>
+    <script src="../external/jquery/jquery-1.8.3.js"></script>
+
+    <script src="../src/jsgrid.core.js"></script>
+    <script src="../src/jsgrid.load-indicator.js"></script>
+    <script src="../src/jsgrid.load-strategies.js"></script>
+    <script src="../src/jsgrid.sort-strategies.js"></script>
+    <script src="../src/jsgrid.field.js"></script>
+    <script src="../src/fields/jsgrid.field.text.js"></script>
+    <script src="../src/fields/jsgrid.field.textarea.js"></script>
+    <script src="../src/fields/jsgrid.field.number.js"></script>
+
+    <style>
+        .rating {
+            color: #F8CA03;
+        }
+    </style>
+</head>
+<body>
+    <h1>Custom Load Indicator</h1>
+    <div id="jsGrid"></div>
+
+    <script>
+        $(function() {
+
+            $("#jsGrid").jsGrid({
+                height: "50%",
+                width: "100%",
+                sorting: true,
+                paging: false,
+                autoload: true,
+                controller: {
+                    loadData: function() {
+                        var d = $.Deferred();
+
+                        $.ajax({
+                            url: "http://services.odata.org/V3/(S(3mnweai3qldmghnzfshavfok))/OData/OData.svc/Products",
+                            dataType: "json"
+                        }).done(function(response) {
+                            setTimeout(function() {
+                                d.resolve(response.value);
+                            }, 2000);
+                        });
+
+                        return d.promise();
+                    }
+                },
+                loadIndicator: function(config) {
+                    var container = config.container[0];
+                    var spinner = new Spinner();
+
+                    return {
+                        show: function() {
+                            spinner.spin(container);
+                        },
+                        hide: function() {
+                            spinner.stop();
+                        }
+                    };
+                },
+                fields: [
+                    { name: "Name", type: "text" },
+                    { name: "Description", type: "textarea", width: 150 },
+                    { name: "Rating", type: "number", width: 50, align: "center",
+                        itemTemplate: function(value) {
+                            return $("<div>").addClass("rating").append(Array(value + 1).join("&#9733;"));
+                        }
+                    },
+                    { name: "Price", type: "number", width: 50,
+                        itemTemplate: function(value) {
+                            return value.toFixed(2) + "$"; }
+                    }
+                ]
+            });
+
+        });
+    </script>
+</body>
+</html>

+ 79 - 0
src/api/static/js/jsgrid/demos/custom-row-renderer.html

@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <title>jsGrid - Custom Row Renderer</title>
+    <link rel="stylesheet" type="text/css" href="demos.css" />
+    <link href='http://fonts.googleapis.com/css?family=Open+Sans:300,600,400' rel='stylesheet' type='text/css'>
+
+    <link rel="stylesheet" type="text/css" href="../css/jsgrid.css" />
+    <link rel="stylesheet" type="text/css" href="../css/theme.css" />
+
+    <script src="../external/jquery/jquery-1.8.3.js"></script>
+
+    <script src="../src/jsgrid.core.js"></script>
+    <script src="../src/jsgrid.load-indicator.js"></script>
+    <script src="../src/jsgrid.load-strategies.js"></script>
+    <script src="../src/jsgrid.sort-strategies.js"></script>
+    <script src="../src/jsgrid.field.js"></script>
+
+    <style>
+        .client-photo { float: left; margin: 0 20px 0 10px; }
+        .client-photo img { border-radius: 50%; border: 1px solid #ddd; }
+        .client-info { margin-top: 10px; }
+        .client-info p { line-height: 25px; }
+    </style>
+</head>
+<body>
+    <h1>Custom Row Renderer</h1>
+    <div id="jsGrid"></div>
+
+    <script>
+        $(function() {
+
+            $("#jsGrid").jsGrid({
+                height: "80%",
+                width: "50%",
+                autoload: true,
+                paging: true,
+                controller: {
+                    loadData: function() {
+                        var deferred = $.Deferred();
+
+                        $.ajax({
+                            url: 'http://api.randomuser.me/?results=40',
+                            dataType: 'jsonp',
+                            success: function(data){
+                                deferred.resolve(data.results);
+                            }
+                        });
+
+                        return deferred.promise();
+                    }
+                },
+                rowRenderer: function(item) {
+                    var user = item;
+                    var $photo = $("<div>").addClass("client-photo").append($("<img>").attr("src", user.picture.large));
+                    var $info = $("<div>").addClass("client-info")
+                        .append($("<p>").append($("<strong>").text(user.name.first.capitalize() + " " + user.name.last.capitalize())))
+                        .append($("<p>").text("Location: " + user.location.city.capitalize() + ", " + user.location.street))
+                        .append($("<p>").text("Email: " + user.email))
+                        .append($("<p>").text("Phone: " + user.phone))
+                        .append($("<p>").text("Cell: " + user.cell));
+
+                    return $("<tr>").append($("<td>").append($photo).append($info));
+                },
+                fields: [
+                    { title: "Clients" }
+                ]
+            });
+
+
+            String.prototype.capitalize = function() {
+                return this.charAt(0).toUpperCase() + this.slice(1);
+            };
+
+        });
+    </script>
+</body>
+</html>

+ 85 - 0
src/api/static/js/jsgrid/demos/custom-view.html

@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <title>jsGrid - Custom View Scenario</title>
+    <link rel="stylesheet" type="text/css" href="demos.css" />
+    <link href='http://fonts.googleapis.com/css?family=Open+Sans:300,600,400' rel='stylesheet' type='text/css'>
+
+    <link rel="stylesheet" type="text/css" href="../css/jsgrid.css" />
+    <link rel="stylesheet" type="text/css" href="../css/theme.css" />
+
+    <script src="../external/jquery/jquery-1.8.3.js"></script>
+    <script src="db.js"></script>
+
+    <script src="../src/jsgrid.core.js"></script>
+    <script src="../src/jsgrid.load-indicator.js"></script>
+    <script src="../src/jsgrid.load-strategies.js"></script>
+    <script src="../src/jsgrid.sort-strategies.js"></script>
+    <script src="../src/jsgrid.field.js"></script>
+    <script src="../src/fields/jsgrid.field.text.js"></script>
+    <script src="../src/fields/jsgrid.field.number.js"></script>
+    <script src="../src/fields/jsgrid.field.select.js"></script>
+    <script src="../src/fields/jsgrid.field.checkbox.js"></script>
+    <script src="../src/fields/jsgrid.field.control.js"></script>
+
+    <style>
+        .config-panel {
+            padding: 10px;
+            margin: 10px 0;
+            background: #fcfcfc;
+            border: 1px solid #e9e9e9;
+            display: inline-block;
+        }
+
+        .config-panel label {
+            margin-right: 10px;
+        }
+    </style>
+</head>
+<body>
+    <h1>Custom View</h1>
+    <div class="config-panel">
+        <label><input id="heading" type="checkbox" checked /> Heading</label>
+        <label><input id="filtering" type="checkbox" checked /> Filtering</label>
+        <label><input id="inserting" type="checkbox" /> Inserting</label>
+        <label><input id="editing" type="checkbox" checked /> Editing</label>
+        <label><input id="paging" type="checkbox" checked /> Paging</label>
+        <label><input id="sorting" type="checkbox" checked /> Sorting</label>
+        <label><input id="selecting" type="checkbox" checked /> Selecting</label>
+    </div>
+
+    <div id="jsGrid"></div>
+
+    <script>
+        $(function() {
+
+            $("#jsGrid").jsGrid({
+                height: "70%",
+                width: "100%",
+                filtering: true,
+                editing: true,
+                sorting: true,
+                paging: true,
+                autoload: true,
+                pageSize: 15,
+                pageButtonCount: 5,
+                controller: db,
+                fields: [
+                    { name: "Name", type: "text", width: 150 },
+                    { name: "Age", type: "number", width: 50 },
+                    { name: "Address", type: "text", width: 200 },
+                    { name: "Country", type: "select", items: db.countries, valueField: "Id", textField: "Name" },
+                    { name: "Married", type: "checkbox", title: "Is Married", sorting: false },
+                    { type: "control", modeSwitchButton: false, editButton: false }
+                ]
+            });
+
+            $(".config-panel input[type=checkbox]").on("click", function() {
+                var $cb = $(this);
+                $("#jsGrid").jsGrid("option", $cb.attr("id"), $cb.is(":checked"));
+            });
+        });
+    </script>
+</body>
+</html>

+ 212 - 0
src/api/static/js/jsgrid/demos/data-manipulation.html

@@ -0,0 +1,212 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <title>jsGrid - Data Manipulation</title>
+    <link rel="stylesheet" type="text/css" href="demos.css" />
+    <link href='http://fonts.googleapis.com/css?family=Open+Sans:300,600,400' rel='stylesheet' type='text/css'>
+
+    <link rel="stylesheet" type="text/css" href="../css/jsgrid.css" />
+    <link rel="stylesheet" type="text/css" href="../css/theme.css" />
+
+    <link rel="stylesheet" href="http://code.jquery.com/ui/1.11.2/themes/cupertino/jquery-ui.css">
+    <script src="http://code.jquery.com/jquery-1.10.2.js"></script>
+    <script src="http://code.jquery.com/ui/1.11.2/jquery-ui.js"></script>
+    <script src="http://ajax.aspnetcdn.com/ajax/jquery.validate/1.9/jquery.validate.min.js"></script>
+
+    <script src="db.js"></script>
+
+    <script src="../src/jsgrid.core.js"></script>
+    <script src="../src/jsgrid.load-indicator.js"></script>
+    <script src="../src/jsgrid.load-strategies.js"></script>
+    <script src="../src/jsgrid.sort-strategies.js"></script>
+    <script src="../src/jsgrid.field.js"></script>
+    <script src="../src/fields/jsgrid.field.text.js"></script>
+    <script src="../src/fields/jsgrid.field.number.js"></script>
+    <script src="../src/fields/jsgrid.field.select.js"></script>
+    <script src="../src/fields/jsgrid.field.checkbox.js"></script>
+    <script src="../src/fields/jsgrid.field.control.js"></script>
+
+    <style>
+        .ui-widget *, .ui-widget input, .ui-widget select, .ui-widget button {
+            font-family: 'Helvetica Neue Light', 'Open Sans', Helvetica;
+            font-size: 14px;
+            font-weight: 300 !important;
+        }
+
+        .details-form-field input,
+        .details-form-field select {
+            width: 250px;
+            float: right;
+        }
+
+        .details-form-field {
+            margin: 30px 0;
+        }
+
+        .details-form-field:first-child {
+            margin-top: 10px;
+        }
+
+        .details-form-field:last-child {
+            margin-bottom: 10px;
+        }
+
+        .details-form-field button {
+            display: block;
+            width: 100px;
+            margin: 0 auto;
+        }
+
+        input.error, select.error {
+            border: 1px solid #ff9999;
+            background: #ffeeee;
+        }
+
+        label.error {
+            float: right;
+            margin-left: 100px;
+            font-size: .8em;
+            color: #ff6666;
+        }
+    </style>
+</head>
+<body>
+    <h1>Data Manipulation</h1>
+    <div id="jsGrid"></div>
+
+    <div id="detailsDialog">
+        <form id="detailsForm">
+            <div class="details-form-field">
+                <label for="name">Name:</label>
+                <input id="name" name="name" type="text" />
+            </div>
+            <div class="details-form-field">
+                <label for="age">Age:</label>
+                <input id="age" name="age" type="number" />
+            </div>
+            <div class="details-form-field">
+                <label for="address">Address:</label>
+                <input id="address" name="address" type="text" />
+            </div>
+            <div class="details-form-field">
+                <label for="country">Country:</label>
+                <select id="country" name="country">
+                    <option value="">(Select)</option>
+                    <option value="1">United States</option>
+                    <option value="2">Canada</option>
+                    <option value="3">United Kingdom</option>
+                    <option value="4">France</option>
+                    <option value="5">Brazil</option>
+                    <option value="6">China</option>
+                    <option value="7">Russia</option>
+                </select>
+            </div>
+            <div class="details-form-field">
+                <label for="married">Is Married</label>
+                <input id="married" name="married" type="checkbox" />
+            </div>
+            <div class="details-form-field">
+                <button type="submit" id="save">Save</button>
+            </div>
+        </form>
+    </div>
+
+    <script>
+        $(function() {
+
+            $("#jsGrid").jsGrid({
+                height: "70%",
+                width: "100%",
+                editing: true,
+                autoload: true,
+                paging: true,
+                deleteConfirm: function(item) {
+                    return "The client \"" + item.Name + "\" will be removed. Are you sure?";
+                },
+                rowClick: function(args) {
+                    showDetailsDialog("Edit", args.item);
+                },
+                controller: db,
+                fields: [
+                    { name: "Name", type: "text", width: 150 },
+                    { name: "Age", type: "number", width: 50 },
+                    { name: "Address", type: "text", width: 200 },
+                    { name: "Country", type: "select", items: db.countries, valueField: "Id", textField: "Name" },
+                    { name: "Married", type: "checkbox", title: "Is Married", sorting: false },
+                    {
+                        type: "control",
+                        modeSwitchButton: false,
+                        editButton: false,
+                        headerTemplate: function() {
+                            return $("<button>").attr("type", "button").text("Add")
+                                .on("click", function () {
+                                    showDetailsDialog("Add", {});
+                                });
+                        }
+                    }
+                ]
+            });
+
+            $("#detailsDialog").dialog({
+                autoOpen: false,
+                width: 400,
+                close: function() {
+                    $("#detailsForm").validate().resetForm();
+                    $("#detailsForm").find(".error").removeClass("error");
+                }
+            });
+
+            $("#detailsForm").validate({
+                rules: {
+                    name: "required",
+                    age: { required: true, range: [18, 150] },
+                    address: { required: true, minlength: 10 },
+                    country: "required"
+                },
+                messages: {
+                    name: "Please enter name",
+                    age: "Please enter valid age",
+                    address: "Please enter address (more than 10 chars)",
+                    country: "Please select country"
+                },
+                submitHandler: function() {
+                    formSubmitHandler();
+                }
+            });
+
+            var formSubmitHandler = $.noop;
+
+            var showDetailsDialog = function(dialogType, client) {
+                $("#name").val(client.Name);
+                $("#age").val(client.Age);
+                $("#address").val(client.Address);
+                $("#country").val(client.Country);
+                $("#married").prop("checked", client.Married);
+
+                formSubmitHandler = function() {
+                    saveClient(client, dialogType === "Add");
+                };
+
+                $("#detailsDialog").dialog("option", "title", dialogType + " Client")
+                    .dialog("open");
+            };
+
+            var saveClient = function(client, isNew) {
+                $.extend(client, {
+                    Name: $("#name").val(),
+                    Age: parseInt($("#age").val(), 10),
+                    Address: $("#address").val(),
+                    Country: parseInt($("#country").val(), 10),
+                    Married: $("#married").is(":checked")
+                });
+
+                $("#jsGrid").jsGrid(isNew ? "insertItem" : "updateItem", client);
+
+                $("#detailsDialog").dialog("close");
+            };
+
+        });
+    </script>
+</body>
+</html>

+ 884 - 0
src/api/static/js/jsgrid/demos/db.js

@@ -0,0 +1,884 @@
+(function() {
+
+    var db = {
+
+        loadData: function(filter) {
+            return $.grep(this.clients, function(client) {
+                return (!filter.Name || client.Name.indexOf(filter.Name) > -1)
+                    && (filter.Age === undefined || client.Age === filter.Age)
+                    && (!filter.Address || client.Address.indexOf(filter.Address) > -1)
+                    && (!filter.Country || client.Country === filter.Country)
+                    && (filter.Married === undefined || client.Married === filter.Married);
+            });
+        },
+
+        insertItem: function(insertingClient) {
+            this.clients.push(insertingClient);
+        },
+
+        updateItem: function(updatingClient) { },
+
+        deleteItem: function(deletingClient) {
+            var clientIndex = $.inArray(deletingClient, this.clients);
+            this.clients.splice(clientIndex, 1);
+        }
+
+    };
+
+    window.db = db;
+
+
+    db.countries = [
+        { Name: "", Id: 0 },
+        { Name: "United States", Id: 1 },
+        { Name: "Canada", Id: 2 },
+        { Name: "United Kingdom", Id: 3 },
+        { Name: "France", Id: 4 },
+        { Name: "Brazil", Id: 5 },
+        { Name: "China", Id: 6 },
+        { Name: "Russia", Id: 7 }
+    ];
+
+    db.clients = [
+        {
+            "Name": "Otto Clay",
+            "Age": 61,
+            "Country": 6,
+            "Address": "Ap #897-1459 Quam Avenue",
+            "Married": false
+        },
+        {
+            "Name": "Connor Johnston",
+            "Age": 73,
+            "Country": 7,
+            "Address": "Ap #370-4647 Dis Av.",
+            "Married": false
+        },
+        {
+            "Name": "Lacey Hess",
+            "Age": 29,
+            "Country": 7,
+            "Address": "Ap #365-8835 Integer St.",
+            "Married": false
+        },
+        {
+            "Name": "Timothy Henson",
+            "Age": 78,
+            "Country": 1,
+            "Address": "911-5143 Luctus Ave",
+            "Married": false
+        },
+        {
+            "Name": "Ramona Benton",
+            "Age": 43,
+            "Country": 5,
+            "Address": "Ap #614-689 Vehicula Street",
+            "Married": true
+        },
+        {
+            "Name": "Ezra Tillman",
+            "Age": 51,
+            "Country": 1,
+            "Address": "P.O. Box 738, 7583 Quisque St.",
+            "Married": true
+        },
+        {
+            "Name": "Dante Carter",
+            "Age": 59,
+            "Country": 1,
+            "Address": "P.O. Box 976, 6316 Lorem, St.",
+            "Married": false
+        },
+        {
+            "Name": "Christopher Mcclure",
+            "Age": 58,
+            "Country": 1,
+            "Address": "847-4303 Dictum Av.",
+            "Married": true
+        },
+        {
+            "Name": "Ruby Rocha",
+            "Age": 62,
+            "Country": 2,
+            "Address": "5212 Sagittis Ave",
+            "Married": false
+        },
+        {
+            "Name": "Imelda Hardin",
+            "Age": 39,
+            "Country": 5,
+            "Address": "719-7009 Auctor Av.",
+            "Married": false
+        },
+        {
+            "Name": "Jonah Johns",
+            "Age": 28,
+            "Country": 5,
+            "Address": "P.O. Box 939, 9310 A Ave",
+            "Married": false
+        },
+        {
+            "Name": "Herman Rosa",
+            "Age": 49,
+            "Country": 7,
+            "Address": "718-7162 Molestie Av.",
+            "Married": true
+        },
+        {
+            "Name": "Arthur Gay",
+            "Age": 20,
+            "Country": 7,
+            "Address": "5497 Neque Street",
+            "Married": false
+        },
+        {
+            "Name": "Xena Wilkerson",
+            "Age": 63,
+            "Country": 1,
+            "Address": "Ap #303-6974 Proin Street",
+            "Married": true
+        },
+        {
+            "Name": "Lilah Atkins",
+            "Age": 33,
+            "Country": 5,
+            "Address": "622-8602 Gravida Ave",
+            "Married": true
+        },
+        {
+            "Name": "Malik Shepard",
+            "Age": 59,
+            "Country": 1,
+            "Address": "967-5176 Tincidunt Av.",
+            "Married": false
+        },
+        {
+            "Name": "Keely Silva",
+            "Age": 24,
+            "Country": 1,
+            "Address": "P.O. Box 153, 8995 Praesent Ave",
+            "Married": false
+        },
+        {
+            "Name": "Hunter Pate",
+            "Age": 73,
+            "Country": 7,
+            "Address": "P.O. Box 771, 7599 Ante, Road",
+            "Married": false
+        },
+        {
+            "Name": "Mikayla Roach",
+            "Age": 55,
+            "Country": 5,
+            "Address": "Ap #438-9886 Donec Rd.",
+            "Married": true
+        },
+        {
+            "Name": "Upton Joseph",
+            "Age": 48,
+            "Country": 4,
+            "Address": "Ap #896-7592 Habitant St.",
+            "Married": true
+        },
+        {
+            "Name": "Jeanette Pate",
+            "Age": 59,
+            "Country": 2,
+            "Address": "P.O. Box 177, 7584 Amet, St.",
+            "Married": false
+        },
+        {
+            "Name": "Kaden Hernandez",
+            "Age": 79,
+            "Country": 3,
+            "Address": "366 Ut St.",
+            "Married": true
+        },
+        {
+            "Name": "Kenyon Stevens",
+            "Age": 20,
+            "Country": 3,
+            "Address": "P.O. Box 704, 4580 Gravida Rd.",
+            "Married": false
+        },
+        {
+            "Name": "Jerome Harper",
+            "Age": 31,
+            "Country": 5,
+            "Address": "2464 Porttitor Road",
+            "Married": false
+        },
+        {
+            "Name": "Jelani Patel",
+            "Age": 36,
+            "Country": 2,
+            "Address": "P.O. Box 541, 5805 Nec Av.",
+            "Married": true
+        },
+        {
+            "Name": "Keaton Oconnor",
+            "Age": 21,
+            "Country": 1,
+            "Address": "Ap #657-1093 Nec, Street",
+            "Married": false
+        },
+        {
+            "Name": "Bree Johnston",
+            "Age": 31,
+            "Country": 2,
+            "Address": "372-5942 Vulputate Avenue",
+            "Married": false
+        },
+        {
+            "Name": "Maisie Hodges",
+            "Age": 70,
+            "Country": 7,
+            "Address": "P.O. Box 445, 3880 Odio, Rd.",
+            "Married": false
+        },
+        {
+            "Name": "Kuame Calhoun",
+            "Age": 39,
+            "Country": 2,
+            "Address": "P.O. Box 609, 4105 Rutrum St.",
+            "Married": true
+        },
+        {
+            "Name": "Carlos Cameron",
+            "Age": 38,
+            "Country": 5,
+            "Address": "Ap #215-5386 A, Avenue",
+            "Married": false
+        },
+        {
+            "Name": "Fulton Parsons",
+            "Age": 25,
+            "Country": 7,
+            "Address": "P.O. Box 523, 3705 Sed Rd.",
+            "Married": false
+        },
+        {
+            "Name": "Wallace Christian",
+            "Age": 43,
+            "Country": 3,
+            "Address": "416-8816 Mauris Avenue",
+            "Married": true
+        },
+        {
+            "Name": "Caryn Maldonado",
+            "Age": 40,
+            "Country": 1,
+            "Address": "108-282 Nonummy Ave",
+            "Married": false
+        },
+        {
+            "Name": "Whilemina Frank",
+            "Age": 20,
+            "Country": 7,
+            "Address": "P.O. Box 681, 3938 Egestas. Av.",
+            "Married": true
+        },
+        {
+            "Name": "Emery Moon",
+            "Age": 41,
+            "Country": 4,
+            "Address": "Ap #717-8556 Non Road",
+            "Married": true
+        },
+        {
+            "Name": "Price Watkins",
+            "Age": 35,
+            "Country": 4,
+            "Address": "832-7810 Nunc Rd.",
+            "Married": false
+        },
+        {
+            "Name": "Lydia Castillo",
+            "Age": 59,
+            "Country": 7,
+            "Address": "5280 Placerat, Ave",
+            "Married": true
+        },
+        {
+            "Name": "Lawrence Conway",
+            "Age": 53,
+            "Country": 1,
+            "Address": "Ap #452-2808 Imperdiet St.",
+            "Married": false
+        },
+        {
+            "Name": "Kalia Nicholson",
+            "Age": 67,
+            "Country": 5,
+            "Address": "P.O. Box 871, 3023 Tellus Road",
+            "Married": true
+        },
+        {
+            "Name": "Brielle Baxter",
+            "Age": 45,
+            "Country": 3,
+            "Address": "Ap #822-9526 Ut, Road",
+            "Married": true
+        },
+        {
+            "Name": "Valentine Brady",
+            "Age": 72,
+            "Country": 7,
+            "Address": "8014 Enim. Road",
+            "Married": true
+        },
+        {
+            "Name": "Rebecca Gardner",
+            "Age": 57,
+            "Country": 4,
+            "Address": "8655 Arcu. Road",
+            "Married": true
+        },
+        {
+            "Name": "Vladimir Tate",
+            "Age": 26,
+            "Country": 1,
+            "Address": "130-1291 Non, Rd.",
+            "Married": true
+        },
+        {
+            "Name": "Vernon Hays",
+            "Age": 56,
+            "Country": 4,
+            "Address": "964-5552 In Rd.",
+            "Married": true
+        },
+        {
+            "Name": "Allegra Hull",
+            "Age": 22,
+            "Country": 4,
+            "Address": "245-8891 Donec St.",
+            "Married": true
+        },
+        {
+            "Name": "Hu Hendrix",
+            "Age": 65,
+            "Country": 7,
+            "Address": "428-5404 Tempus Ave",
+            "Married": true
+        },
+        {
+            "Name": "Kenyon Battle",
+            "Age": 32,
+            "Country": 2,
+            "Address": "921-6804 Lectus St.",
+            "Married": false
+        },
+        {
+            "Name": "Gloria Nielsen",
+            "Age": 24,
+            "Country": 4,
+            "Address": "Ap #275-4345 Lorem, Street",
+            "Married": true
+        },
+        {
+            "Name": "Illiana Kidd",
+            "Age": 59,
+            "Country": 2,
+            "Address": "7618 Lacus. Av.",
+            "Married": false
+        },
+        {
+            "Name": "Adria Todd",
+            "Age": 68,
+            "Country": 6,
+            "Address": "1889 Tincidunt Road",
+            "Married": false
+        },
+        {
+            "Name": "Kirsten Mayo",
+            "Age": 71,
+            "Country": 1,
+            "Address": "100-8640 Orci, Avenue",
+            "Married": false
+        },
+        {
+            "Name": "Willa Hobbs",
+            "Age": 60,
+            "Country": 6,
+            "Address": "P.O. Box 323, 158 Tristique St.",
+            "Married": false
+        },
+        {
+            "Name": "Alexis Clements",
+            "Age": 69,
+            "Country": 5,
+            "Address": "P.O. Box 176, 5107 Proin Rd.",
+            "Married": false
+        },
+        {
+            "Name": "Akeem Conrad",
+            "Age": 60,
+            "Country": 2,
+            "Address": "282-495 Sed Ave",
+            "Married": true
+        },
+        {
+            "Name": "Montana Silva",
+            "Age": 79,
+            "Country": 6,
+            "Address": "P.O. Box 120, 9766 Consectetuer St.",
+            "Married": false
+        },
+        {
+            "Name": "Kaseem Hensley",
+            "Age": 77,
+            "Country": 6,
+            "Address": "Ap #510-8903 Mauris. Av.",
+            "Married": true
+        },
+        {
+            "Name": "Christopher Morton",
+            "Age": 35,
+            "Country": 5,
+            "Address": "P.O. Box 234, 3651 Sodales Avenue",
+            "Married": false
+        },
+        {
+            "Name": "Wade Fernandez",
+            "Age": 49,
+            "Country": 6,
+            "Address": "740-5059 Dolor. Road",
+            "Married": true
+        },
+        {
+            "Name": "Illiana Kirby",
+            "Age": 31,
+            "Country": 2,
+            "Address": "527-3553 Mi Ave",
+            "Married": false
+        },
+        {
+            "Name": "Kimberley Hurley",
+            "Age": 65,
+            "Country": 5,
+            "Address": "P.O. Box 637, 9915 Dictum St.",
+            "Married": false
+        },
+        {
+            "Name": "Arthur Olsen",
+            "Age": 74,
+            "Country": 5,
+            "Address": "887-5080 Eget St.",
+            "Married": false
+        },
+        {
+            "Name": "Brody Potts",
+            "Age": 59,
+            "Country": 2,
+            "Address": "Ap #577-7690 Sem Road",
+            "Married": false
+        },
+        {
+            "Name": "Dillon Ford",
+            "Age": 60,
+            "Country": 1,
+            "Address": "Ap #885-9289 A, Av.",
+            "Married": true
+        },
+        {
+            "Name": "Hannah Juarez",
+            "Age": 61,
+            "Country": 2,
+            "Address": "4744 Sapien, Rd.",
+            "Married": true
+        },
+        {
+            "Name": "Vincent Shaffer",
+            "Age": 25,
+            "Country": 2,
+            "Address": "9203 Nunc St.",
+            "Married": true
+        },
+        {
+            "Name": "George Holt",
+            "Age": 27,
+            "Country": 6,
+            "Address": "4162 Cras Rd.",
+            "Married": false
+        },
+        {
+            "Name": "Tobias Bartlett",
+            "Age": 74,
+            "Country": 4,
+            "Address": "792-6145 Mauris St.",
+            "Married": true
+        },
+        {
+            "Name": "Xavier Hooper",
+            "Age": 35,
+            "Country": 1,
+            "Address": "879-5026 Interdum. Rd.",
+            "Married": false
+        },
+        {
+            "Name": "Declan Dorsey",
+            "Age": 31,
+            "Country": 2,
+            "Address": "Ap #926-4171 Aenean Road",
+            "Married": true
+        },
+        {
+            "Name": "Clementine Tran",
+            "Age": 43,
+            "Country": 4,
+            "Address": "P.O. Box 176, 9865 Eu Rd.",
+            "Married": true
+        },
+        {
+            "Name": "Pamela Moody",
+            "Age": 55,
+            "Country": 6,
+            "Address": "622-6233 Luctus Rd.",
+            "Married": true
+        },
+        {
+            "Name": "Julie Leon",
+            "Age": 43,
+            "Country": 6,
+            "Address": "Ap #915-6782 Sem Av.",
+            "Married": true
+        },
+        {
+            "Name": "Shana Nolan",
+            "Age": 79,
+            "Country": 5,
+            "Address": "P.O. Box 603, 899 Eu St.",
+            "Married": false
+        },
+        {
+            "Name": "Vaughan Moody",
+            "Age": 37,
+            "Country": 5,
+            "Address": "880 Erat Rd.",
+            "Married": false
+        },
+        {
+            "Name": "Randall Reeves",
+            "Age": 44,
+            "Country": 3,
+            "Address": "1819 Non Street",
+            "Married": false
+        },
+        {
+            "Name": "Dominic Raymond",
+            "Age": 68,
+            "Country": 1,
+            "Address": "Ap #689-4874 Nisi Rd.",
+            "Married": true
+        },
+        {
+            "Name": "Lev Pugh",
+            "Age": 69,
+            "Country": 5,
+            "Address": "Ap #433-6844 Auctor Avenue",
+            "Married": true
+        },
+        {
+            "Name": "Desiree Hughes",
+            "Age": 80,
+            "Country": 4,
+            "Address": "605-6645 Fermentum Avenue",
+            "Married": true
+        },
+        {
+            "Name": "Idona Oneill",
+            "Age": 23,
+            "Country": 7,
+            "Address": "751-8148 Aliquam Avenue",
+            "Married": true
+        },
+        {
+            "Name": "Lani Mayo",
+            "Age": 76,
+            "Country": 1,
+            "Address": "635-2704 Tristique St.",
+            "Married": true
+        },
+        {
+            "Name": "Cathleen Bonner",
+            "Age": 40,
+            "Country": 1,
+            "Address": "916-2910 Dolor Av.",
+            "Married": false
+        },
+        {
+            "Name": "Sydney Murray",
+            "Age": 44,
+            "Country": 5,
+            "Address": "835-2330 Fringilla St.",
+            "Married": false
+        },
+        {
+            "Name": "Brenna Rodriguez",
+            "Age": 77,
+            "Country": 6,
+            "Address": "3687 Imperdiet Av.",
+            "Married": true
+        },
+        {
+            "Name": "Alfreda Mcdaniel",
+            "Age": 38,
+            "Country": 7,
+            "Address": "745-8221 Aliquet Rd.",
+            "Married": true
+        },
+        {
+            "Name": "Zachery Atkins",
+            "Age": 30,
+            "Country": 1,
+            "Address": "549-2208 Auctor. Road",
+            "Married": true
+        },
+        {
+            "Name": "Amelia Rich",
+            "Age": 56,
+            "Country": 4,
+            "Address": "P.O. Box 734, 4717 Nunc Rd.",
+            "Married": false
+        },
+        {
+            "Name": "Kiayada Witt",
+            "Age": 62,
+            "Country": 3,
+            "Address": "Ap #735-3421 Malesuada Avenue",
+            "Married": false
+        },
+        {
+            "Name": "Lysandra Pierce",
+            "Age": 36,
+            "Country": 1,
+            "Address": "Ap #146-2835 Curabitur St.",
+            "Married": true
+        },
+        {
+            "Name": "Cara Rios",
+            "Age": 58,
+            "Country": 4,
+            "Address": "Ap #562-7811 Quam. Ave",
+            "Married": true
+        },
+        {
+            "Name": "Austin Andrews",
+            "Age": 55,
+            "Country": 7,
+            "Address": "P.O. Box 274, 5505 Sociis Rd.",
+            "Married": false
+        },
+        {
+            "Name": "Lillian Peterson",
+            "Age": 39,
+            "Country": 2,
+            "Address": "6212 A Avenue",
+            "Married": false
+        },
+        {
+            "Name": "Adria Beach",
+            "Age": 29,
+            "Country": 2,
+            "Address": "P.O. Box 183, 2717 Nunc Avenue",
+            "Married": true
+        },
+        {
+            "Name": "Oleg Durham",
+            "Age": 80,
+            "Country": 4,
+            "Address": "931-3208 Nunc Rd.",
+            "Married": false
+        },
+        {
+            "Name": "Casey Reese",
+            "Age": 60,
+            "Country": 4,
+            "Address": "383-3675 Ultrices, St.",
+            "Married": false
+        },
+        {
+            "Name": "Kane Burnett",
+            "Age": 80,
+            "Country": 1,
+            "Address": "759-8212 Dolor. Ave",
+            "Married": false
+        },
+        {
+            "Name": "Stewart Wilson",
+            "Age": 46,
+            "Country": 7,
+            "Address": "718-7845 Sagittis. Av.",
+            "Married": false
+        },
+        {
+            "Name": "Charity Holcomb",
+            "Age": 31,
+            "Country": 6,
+            "Address": "641-7892 Enim. Ave",
+            "Married": false
+        },
+        {
+            "Name": "Kyra Cummings",
+            "Age": 43,
+            "Country": 4,
+            "Address": "P.O. Box 702, 6621 Mus. Av.",
+            "Married": false
+        },
+        {
+            "Name": "Stuart Wallace",
+            "Age": 25,
+            "Country": 7,
+            "Address": "648-4990 Sed Rd.",
+            "Married": true
+        },
+        {
+            "Name": "Carter Clarke",
+            "Age": 59,
+            "Country": 6,
+            "Address": "Ap #547-2921 A Street",
+            "Married": false
+        }
+    ];
+
+    db.users = [
+        {
+            "ID": "x",
+            "Account": "A758A693-0302-03D1-AE53-EEFE22855556",
+            "Name": "Carson Kelley",
+            "RegisterDate": "2002-04-20T22:55:52-07:00"
+        },
+        {
+            "Account": "D89FF524-1233-0CE7-C9E1-56EFF017A321",
+            "Name": "Prescott Griffin",
+            "RegisterDate": "2011-02-22T05:59:55-08:00"
+        },
+        {
+            "Account": "06FAAD9A-5114-08F6-D60C-961B2528B4F0",
+            "Name": "Amir Saunders",
+            "RegisterDate": "2014-08-13T09:17:49-07:00"
+        },
+        {
+            "Account": "EED7653D-7DD9-A722-64A8-36A55ECDBE77",
+            "Name": "Derek Thornton",
+            "RegisterDate": "2012-02-27T01:31:07-08:00"
+        },
+        {
+            "Account": "2A2E6D40-FEBD-C643-A751-9AB4CAF1E2F6",
+            "Name": "Fletcher Romero",
+            "RegisterDate": "2010-06-25T15:49:54-07:00"
+        },
+        {
+            "Account": "3978F8FA-DFF0-DA0E-0A5D-EB9D281A3286",
+            "Name": "Thaddeus Stein",
+            "RegisterDate": "2013-11-10T07:29:41-08:00"
+        },
+        {
+            "Account": "658DBF5A-176E-569A-9273-74FB5F69FA42",
+            "Name": "Nash Knapp",
+            "RegisterDate": "2005-06-24T09:11:19-07:00"
+        },
+        {
+            "Account": "76D2EE4B-7A73-1212-F6F2-957EF8C1F907",
+            "Name": "Quamar Vega",
+            "RegisterDate": "2011-04-13T20:06:29-07:00"
+        },
+        {
+            "Account": "00E46809-A595-CE82-C5B4-D1CAEB7E3E58",
+            "Name": "Philip Galloway",
+            "RegisterDate": "2008-08-21T18:59:38-07:00"
+        },
+        {
+            "Account": "C196781C-DDCC-AF83-DDC2-CA3E851A47A0",
+            "Name": "Mason French",
+            "RegisterDate": "2000-11-15T00:38:37-08:00"
+        },
+        {
+            "Account": "5911F201-818A-B393-5888-13157CE0D63F",
+            "Name": "Ross Cortez",
+            "RegisterDate": "2010-05-27T17:35:32-07:00"
+        },
+        {
+            "Account": "B8BB78F9-E1A1-A956-086F-E12B6FE168B6",
+            "Name": "Logan King",
+            "RegisterDate": "2003-07-08T16:58:06-07:00"
+        },
+        {
+            "Account": "06F636C3-9599-1A2D-5FD5-86B24ADDE626",
+            "Name": "Cedric Leblanc",
+            "RegisterDate": "2011-06-30T14:30:10-07:00"
+        },
+        {
+            "Account": "FE880CDD-F6E7-75CB-743C-64C6DE192412",
+            "Name": "Simon Sullivan",
+            "RegisterDate": "2013-06-11T16:35:07-07:00"
+        },
+        {
+            "Account": "BBEDD673-E2C1-4872-A5D3-C4EBD4BE0A12",
+            "Name": "Jamal West",
+            "RegisterDate": "2001-03-16T20:18:29-08:00"
+        },
+        {
+            "Account": "19BC22FA-C52E-0CC6-9552-10365C755FAC",
+            "Name": "Hector Morales",
+            "RegisterDate": "2012-11-01T01:56:34-07:00"
+        },
+        {
+            "Account": "A8292214-2C13-5989-3419-6B83DD637D6C",
+            "Name": "Herrod Hart",
+            "RegisterDate": "2008-03-13T19:21:04-07:00"
+        },
+        {
+            "Account": "0285564B-F447-0E7F-EAA1-7FB8F9C453C8",
+            "Name": "Clark Maxwell",
+            "RegisterDate": "2004-08-05T08:22:24-07:00"
+        },
+        {
+            "Account": "EA78F076-4F6E-4228-268C-1F51272498AE",
+            "Name": "Reuben Walter",
+            "RegisterDate": "2011-01-23T01:55:59-08:00"
+        },
+        {
+            "Account": "6A88C194-EA21-426F-4FE2-F2AE33F51793",
+            "Name": "Ira Ingram",
+            "RegisterDate": "2008-08-15T05:57:46-07:00"
+        },
+        {
+            "Account": "4275E873-439C-AD26-56B3-8715E336508E",
+            "Name": "Damian Morrow",
+            "RegisterDate": "2015-09-13T01:50:55-07:00"
+        },
+        {
+            "Account": "A0D733C4-9070-B8D6-4387-D44F0BA515BE",
+            "Name": "Macon Farrell",
+            "RegisterDate": "2011-03-14T05:41:40-07:00"
+        },
+        {
+            "Account": "B3683DE8-C2FA-7CA0-A8A6-8FA7E954F90A",
+            "Name": "Joel Galloway",
+            "RegisterDate": "2003-02-03T04:19:01-08:00"
+        },
+        {
+            "Account": "01D95A8E-91BC-2050-F5D0-4437AAFFD11F",
+            "Name": "Rigel Horton",
+            "RegisterDate": "2015-06-20T11:53:11-07:00"
+        },
+        {
+            "Account": "F0D12CC0-31AC-A82E-FD73-EEEFDBD21A36",
+            "Name": "Sylvester Gaines",
+            "RegisterDate": "2004-03-12T09:57:13-08:00"
+        },
+        {
+            "Account": "874FCC49-9A61-71BC-2F4E-2CE88348AD7B",
+            "Name": "Abbot Mckay",
+            "RegisterDate": "2008-12-26T20:42:57-08:00"
+        },
+        {
+            "Account": "B8DA1912-20A0-FB6E-0031-5F88FD63EF90",
+            "Name": "Solomon Green",
+            "RegisterDate": "2013-09-04T01:44:47-07:00"
+        }
+     ];
+
+}());

+ 82 - 0
src/api/static/js/jsgrid/demos/demos.css

@@ -0,0 +1,82 @@
+* {
+    margin: 0;
+    padding: 0;
+    box-sizing: border-box;
+}
+
+html {
+    height: 100%;
+}
+
+body {
+    height: 100%;
+    padding: 10px;
+    color: #262626;
+    font-family: 'Helvetica Neue Light', 'Open Sans', Helvetica;
+    font-size: 14px;
+    font-weight: 300;
+}
+
+h1 {
+    margin: 0 0 8px 0;
+    font-size: 24px;
+    font-family: 'Helvetica Neue Light', 'Open Sans', Helvetica;
+    font-weight: 300;
+}
+
+h2 {
+    margin: 16px 0 8px 0;
+    font-size: 18px;
+    font-family: 'Helvetica Neue Light', 'Open Sans', Helvetica;
+    font-weight: 300;
+}
+
+ul {
+    list-style: none;
+}
+
+a {
+    color: #2ba6cb;
+    text-decoration: none;
+}
+
+a:hover {
+    text-decoration: underline;
+    color: #258faf;
+}
+
+input, button, select {
+    font-family: 'Helvetica Neue Light', 'Open Sans', Helvetica;
+    font-weight: 300;
+    font-size: 14px;
+    padding: 2px;
+}
+
+.navigation {
+    width: 200px;
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    padding: 10px;
+    border-right: 1px solid #e9e9e9;
+}
+
+.navigation li {
+    margin: 10px 0;
+}
+
+.demo-frame {
+    position: absolute;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 200px;
+}
+
+iframe[name='demo'] {
+    display: block;
+    width: 100%;
+    height: 100%;
+    border: none;
+}

+ 72 - 0
src/api/static/js/jsgrid/demos/external-pager.html

@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <title>jsGrid - External Pager Scenario</title>
+    <link rel="stylesheet" type="text/css" href="demos.css" />
+    <link href='http://fonts.googleapis.com/css?family=Open+Sans:300,600,400' rel='stylesheet' type='text/css'>
+
+    <link rel="stylesheet" type="text/css" href="../css/jsgrid.css" />
+    <link rel="stylesheet" type="text/css" href="../css/theme.css" />
+
+    <script src="../external/jquery/jquery-1.8.3.js"></script>
+    <script src="db.js"></script>
+
+    <script src="../src/jsgrid.core.js"></script>
+    <script src="../src/jsgrid.load-indicator.js"></script>
+    <script src="../src/jsgrid.load-strategies.js"></script>
+    <script src="../src/jsgrid.sort-strategies.js"></script>
+    <script src="../src/jsgrid.field.js"></script>
+    <script src="../src/fields/jsgrid.field.text.js"></script>
+    <script src="../src/fields/jsgrid.field.number.js"></script>
+    <script src="../src/fields/jsgrid.field.select.js"></script>
+    <script src="../src/fields/jsgrid.field.checkbox.js"></script>
+
+    <style>
+        .external-pager {
+            margin: 10px 0;
+        }
+
+        .external-pager .jsgrid-pager-current-page {
+            background: #c4e2ff;
+            color: #fff;
+        }
+    </style>
+</head>
+<body>
+    <h1>External Customized Pager</h1>
+    <div id="externalPager" class="external-pager"></div>
+
+    <div id="jsGrid"></div>
+
+    <script>
+        $(function() {
+
+            $("#jsGrid").jsGrid({
+                height: "70%",
+                width: "100%",
+                paging: true,
+                pageSize: 15,
+                pageButtonCount: 5,
+                pagerContainer: "#externalPager",
+                pagerFormat: "current page: {pageIndex} &nbsp;&nbsp; {first} {prev} {pages} {next} {last} &nbsp;&nbsp; total pages: {pageCount} total items: {itemCount}",
+                pagePrevText: "<",
+                pageNextText: ">",
+                pageFirstText: "<<",
+                pageLastText: ">>",
+                pageNavigatorNextText: "&#8230;",
+                pageNavigatorPrevText: "&#8230;",
+                fields: [
+                    { name: "Name", type: "text", width: 150 },
+                    { name: "Age", type: "number", width: 50 },
+                    { name: "Address", type: "text", width: 200 },
+                    { name: "Country", type: "select", items: db.countries, valueField: "Id", textField: "Name" },
+                    { name: "Married", type: "checkbox", title: "Is Married" }
+                ],
+                data: db.clients
+            });
+
+        });
+    </script>
+</body>
+</html>

+ 31 - 0
src/api/static/js/jsgrid/demos/index.html

@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <title>jsGrid - Simple jQuery DataGrid - Demos</title>
+    <link rel="stylesheet" type="text/css" href="demos.css" />
+    <link href='http://fonts.googleapis.com/css?family=Open+Sans:300,600,400' rel='stylesheet' type='text/css'>
+</head>
+<body>
+    <div class="navigation">
+        <h1>jsGrid Demos</h1>
+        <ul>
+            <li><a href="basic.html" target="demo">Basic Scenario</a></li>
+            <li><a href="static-data.html" target="demo">Static Data</a></li>
+            <li><a href="odata-service.html" target="demo">OData Service</a></li>
+            <li><a href="data-manipulation.html" target="demo">Data Manipulation</a></li>
+            <li><a href="validation.html" target="demo">Validation</a></li>
+            <li><a href="sorting.html" target="demo">Sorting</a></li>
+            <li><a href="loading-by-page.html" target="demo">Loading by Page</a></li>
+            <li><a href="custom-view.html" target="demo">Custom View</a></li>
+            <li><a href="custom-row-renderer.html" target="demo">Custom Row Renderer</a></li>
+            <li><a href="external-pager.html" target="demo">External Pager</a></li>
+            <li><a href="custom-grid-field.html" target="demo">Custom Grid Field</a></li>
+            <li><a href="localization.html" target="demo">Localization</a></li>
+        </ul>
+    </div>
+    <div class="demo-frame">
+        <iframe name="demo" src="basic.html"></iframe>
+    </div>
+</body>
+</html>

+ 90 - 0
src/api/static/js/jsgrid/demos/loading-by-page.html

@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <title>jsGrid - Loading Data by Page Scenario</title>
+    <link rel="stylesheet" type="text/css" href="demos.css" />
+    <link href='http://fonts.googleapis.com/css?family=Open+Sans:300,600,400' rel='stylesheet' type='text/css'>
+
+    <link rel="stylesheet" type="text/css" href="../css/jsgrid.css" />
+    <link rel="stylesheet" type="text/css" href="../css/theme.css" />
+
+    <script src="../external/jquery/jquery-1.8.3.js"></script>
+    <script src="db.js"></script>
+
+    <script src="../src/jsgrid.core.js"></script>
+    <script src="../src/jsgrid.load-indicator.js"></script>
+    <script src="../src/jsgrid.load-strategies.js"></script>
+    <script src="../src/jsgrid.sort-strategies.js"></script>
+    <script src="../src/jsgrid.field.js"></script>
+    <script src="../src/fields/jsgrid.field.text.js"></script>
+    <script src="../src/fields/jsgrid.field.number.js"></script>
+    <script src="../src/fields/jsgrid.field.select.js"></script>
+    <script src="../src/fields/jsgrid.field.checkbox.js"></script>
+
+    <style>
+        .pager-panel {
+            padding: 10px;
+            margin: 10px 0;
+            background: #fcfcfc;
+            border: 1px solid #e9e9e9;
+            display: inline-block;
+        }
+    </style>
+</head>
+<body>
+    <h1>Loading Data by Page</h1>
+    <div class="pager-panel">
+        <label>Page:
+            <select id="pager">
+                <option>1</option>
+                <option selected>2</option>
+                <option>3</option>
+                <option>4</option>
+                <option>5</option>
+                <option>6</option>
+                <option>7</option>
+            </select>
+        </label>
+    </div>
+
+    <div id="jsGrid"></div>
+
+    <script>
+        $(function() {
+
+            $("#jsGrid").jsGrid({
+                height: "70%",
+                width: "100%",
+                autoload: true,
+                paging: true,
+                pageLoading: true,
+                pageSize: 15,
+                pageIndex: 2,
+                controller: {
+                    loadData: function(filter) {
+                        var startIndex = (filter.pageIndex - 1) * filter.pageSize;
+                        return {
+                            data: db.clients.slice(startIndex, startIndex + filter.pageSize),
+                            itemsCount: db.clients.length
+                        };
+                    }
+                },
+                fields: [
+                    { name: "Name", type: "text", width: 150 },
+                    { name: "Age", type: "number", width: 50 },
+                    { name: "Address", type: "text", width: 200 },
+                    { name: "Country", type: "select", items: db.countries, valueField: "Id", textField: "Name" },
+                    { name: "Married", type: "checkbox", title: "Is Married" }
+                ]
+            });
+
+            $("#pager").on("change", function() {
+                var page = parseInt($(this).val(), 10);
+                $("#jsGrid").jsGrid("openPage", page);
+            });
+
+        });
+    </script>
+</body>
+</html>

+ 62 - 0
src/api/static/js/jsgrid/demos/localization.html

@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <title>jsGrid - Localization (FR)</title>
+    <link rel="stylesheet" type="text/css" href="demos.css" />
+    <link href='http://fonts.googleapis.com/css?family=Open+Sans:300,600,400' rel='stylesheet' type='text/css'>
+
+    <link rel="stylesheet" type="text/css" href="../css/jsgrid.css" />
+    <link rel="stylesheet" type="text/css" href="../css/theme.css" />
+
+    <script src="../external/jquery/jquery-1.8.3.js"></script>
+    <script src="db.js"></script>
+
+    <script src="../src/jsgrid.core.js"></script>
+    <script src="../src/jsgrid.load-indicator.js"></script>
+    <script src="../src/jsgrid.load-strategies.js"></script>
+    <script src="../src/jsgrid.sort-strategies.js"></script>
+    <script src="../src/jsgrid.validation.js"></script>
+    <script src="../src/jsgrid.field.js"></script>
+    <script src="../src/fields/jsgrid.field.text.js"></script>
+    <script src="../src/fields/jsgrid.field.number.js"></script>
+    <script src="../src/fields/jsgrid.field.select.js"></script>
+    <script src="../src/fields/jsgrid.field.checkbox.js"></script>
+    <script src="../src/fields/jsgrid.field.control.js"></script>
+    <script src="../src/i18n/fr.js"></script>
+</head>
+<body>
+    <h1>Localization (FR)</h1>
+    <div id="jsGrid"></div>
+
+    <script>
+        $(function() {
+
+            jsGrid.locale("fr");
+
+            $("#jsGrid").jsGrid({
+                height: "70%",
+                width: "100%",
+                filtering: true,
+                editing: true,
+                inserting: true,
+                sorting: true,
+                paging: true,
+                autoload: true,
+                pageSize: 15,
+                pageButtonCount: 5,
+                controller: db,
+                fields: [
+                    { name: "Name", title: "Nom", type: "text", width: 150, validate: "required" },
+                    { name: "Age", title: "Âge", type: "number", width: 50, validate: { validator: "range", param: [18,80] } },
+                    { name: "Address", title: "Adresse", type: "text", width: 200, validate: { validator: "rangeLength", param: [10, 250] } },
+                    { name: "Country", title: "Pays", type: "select", items: db.countries, valueField: "Id", textField: "Name" },
+                    { name: "Married", title: "Marié", type: "checkbox", sorting: false },
+                    { type: "control" }
+                ]
+            });
+
+        });
+    </script>
+</body>
+</html>

+ 74 - 0
src/api/static/js/jsgrid/demos/odata-service.html

@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <title>jsGrid - OData Service Scenario</title>
+    <link rel="stylesheet" type="text/css" href="demos.css" />
+    <link href='http://fonts.googleapis.com/css?family=Open+Sans:300,600,400' rel='stylesheet' type='text/css'>
+
+    <link rel="stylesheet" type="text/css" href="../css/jsgrid.css" />
+    <link rel="stylesheet" type="text/css" href="../css/theme.css" />
+
+    <script src="../external/jquery/jquery-1.8.3.js"></script>
+
+    <script src="../src/jsgrid.core.js"></script>
+    <script src="../src/jsgrid.load-indicator.js"></script>
+    <script src="../src/jsgrid.load-strategies.js"></script>
+    <script src="../src/jsgrid.sort-strategies.js"></script>
+    <script src="../src/jsgrid.field.js"></script>
+    <script src="../src/fields/jsgrid.field.text.js"></script>
+    <script src="../src/fields/jsgrid.field.textarea.js"></script>
+    <script src="../src/fields/jsgrid.field.number.js"></script>
+
+    <style>
+        .rating {
+            color: #F8CA03;
+        }
+    </style>
+</head>
+<body>
+    <h1>OData Service</h1>
+    <div id="jsGrid"></div>
+
+    <script>
+        $(function() {
+
+            $("#jsGrid").jsGrid({
+                height: "auto",
+                width: "auto",
+                sorting: true,
+                paging: false,
+                autoload: true,
+                controller: {
+                    loadData: function() {
+                        var d = $.Deferred();
+
+                        $.ajax({
+                            url: "http://services.odata.org/V3/(S(3mnweai3qldmghnzfshavfok))/OData/OData.svc/Products",
+                            dataType: "json"
+                        }).done(function(response) {
+                            d.resolve(response.value);
+                        });
+
+                        return d.promise();
+                    }
+                },
+                fields: [
+                    { name: "Name", type: "text", width: 100 },
+                    { name: "Description", type: "textarea", width: 200 },
+                    { name: "Rating", type: "number", width: 150, align: "center",
+                        itemTemplate: function(value) {
+                            return $("<div>").addClass("rating").append(Array(value + 1).join("&#9733;"));
+                        }
+                    },
+                    { name: "Price", type: "number", width: 100,
+                        itemTemplate: function(value) {
+                            return value.toFixed(2) + "$"; }
+                    }
+                ]
+            });
+
+        });
+    </script>
+</body>
+</html>

+ 83 - 0
src/api/static/js/jsgrid/demos/rows-reordering.html

@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <title>jsGrid - Rows Reordering Scenario</title>
+    <link rel="stylesheet" type="text/css" href="demos.css" />
+    <link href='http://fonts.googleapis.com/css?family=Open+Sans:300,600,400' rel='stylesheet' type='text/css'>
+
+    <link rel="stylesheet" type="text/css" href="../css/jsgrid.css" />
+    <link rel="stylesheet" type="text/css" href="../css/theme.css" />
+
+    <link rel="stylesheet" href="http://code.jquery.com/ui/1.11.2/themes/cupertino/jquery-ui.css">
+    <script src="http://code.jquery.com/jquery-1.10.2.js"></script>
+    <script src="http://code.jquery.com/ui/1.11.2/jquery-ui.js"></script>
+    <script src="db.js"></script>
+
+    <script src="../src/jsgrid.core.js"></script>
+    <script src="../src/jsgrid.load-indicator.js"></script>
+    <script src="../src/jsgrid.load-strategies.js"></script>
+    <script src="../src/jsgrid.sort-strategies.js"></script>
+    <script src="../src/jsgrid.field.js"></script>
+    <script src="../src/fields/jsgrid.field.text.js"></script>
+    <script src="../src/fields/jsgrid.field.number.js"></script>
+    <script src="../src/fields/jsgrid.field.select.js"></script>
+    <script src="../src/fields/jsgrid.field.checkbox.js"></script>
+    <script src="../src/fields/jsgrid.field.control.js"></script>
+</head>
+<body>
+<h1>Rows Reordering Scenario</h1>
+<div id="jsGrid"></div>
+
+<script>
+    $(function() {
+
+        $("#jsGrid").jsGrid({
+            height: "70%",
+            width: "100%",
+            autoload: true,
+
+            rowClass: function(item, itemIndex) {
+                return "client-" + itemIndex;
+            },
+
+            controller: {
+                loadData: function() {
+                    return db.clients.slice(0, 15);
+                }
+            },
+
+            fields: [
+                { name: "Name", type: "text", width: 150 },
+                { name: "Age", type: "number", width: 50 },
+                { name: "Address", type: "text", width: 200 },
+                { name: "Country", type: "select", items: db.countries, valueField: "Id", textField: "Name" },
+                { name: "Married", type: "checkbox", title: "Is Married", sorting: false }
+            ],
+
+            onRefreshed: function() {
+                var $gridData = $("#jsGrid .jsgrid-grid-body tbody");
+
+                $gridData.sortable({
+                    update: function(e, ui) {
+                        // array of indexes
+                        var clientIndexRegExp = /\s*client-(\d+)\s*/;
+                        var indexes = $.map($gridData.sortable("toArray", { attribute: "class" }), function(classes) {
+                            return clientIndexRegExp.exec(classes)[1];
+                        });
+                        alert("Reordered indexes: " + indexes.join(", "));
+
+                        // arrays of items
+                        var items = $.map($gridData.find("tr"), function(row) {
+                            return $(row).data("JSGridItem");
+                        });
+                        console && console.log("Reordered items", items);
+                    }
+                });
+            }
+        });
+
+    });
+</script>
+</body>
+</html>

+ 78 - 0
src/api/static/js/jsgrid/demos/sorting.html

@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <title>jsGrid - Sorting Scenario</title>
+    <link rel="stylesheet" type="text/css" href="demos.css" />
+    <link href='http://fonts.googleapis.com/css?family=Open+Sans:300,600,400' rel='stylesheet' type='text/css'>
+
+    <link rel="stylesheet" type="text/css" href="../css/jsgrid.css" />
+    <link rel="stylesheet" type="text/css" href="../css/theme.css" />
+
+    <script src="../external/jquery/jquery-1.8.3.js"></script>
+    <script src="db.js"></script>
+
+    <script src="../src/jsgrid.core.js"></script>
+    <script src="../src/jsgrid.load-indicator.js"></script>
+    <script src="../src/jsgrid.load-strategies.js"></script>
+    <script src="../src/jsgrid.sort-strategies.js"></script>
+    <script src="../src/jsgrid.field.js"></script>
+    <script src="../src/fields/jsgrid.field.text.js"></script>
+    <script src="../src/fields/jsgrid.field.number.js"></script>
+    <script src="../src/fields/jsgrid.field.select.js"></script>
+    <script src="../src/fields/jsgrid.field.checkbox.js"></script>
+
+    <style>
+        .sort-panel {
+            padding: 10px;
+            margin: 10px 0;
+            background: #fcfcfc;
+            border: 1px solid #e9e9e9;
+            display: inline-block;
+        }
+    </style>
+</head>
+<body>
+    <h1>Sorting</h1>
+    <div class="sort-panel">
+        <label>Sorting Field:
+            <select id="sortingField">
+                <option>Name</option>
+                <option>Age</option>
+                <option>Address</option>
+                <option>Country</option>
+                <option>Married</option>
+            </select>
+            <button type="button" id="sort">Sort</button>
+        </label>
+    </div>
+
+    <div id="jsGrid"></div>
+
+    <script>
+        $(function() {
+
+            $("#jsGrid").jsGrid({
+                height: "70%",
+                width: "100%",
+                autoload: true,
+                selecting: false,
+                controller: db,
+                fields: [
+                    { name: "Name", type: "text", width: 150 },
+                    { name: "Age", type: "number", width: 50 },
+                    { name: "Address", type: "text", width: 200 },
+                    { name: "Country", type: "select", items: db.countries, valueField: "Id", textField: "Name" },
+                    { name: "Married", type: "checkbox", title: "Is Married" }
+                ]
+            });
+
+            $("#sort").click(function() {
+                var field = $("#sortingField").val();
+                $("#jsGrid").jsGrid("sort", field);
+            });
+
+        });
+    </script>
+</body>
+</html>

+ 50 - 0
src/api/static/js/jsgrid/demos/static-data.html

@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <title>jsGrid - Static Data Scenario</title>
+    <link rel="stylesheet" type="text/css" href="demos.css" />
+    <link href='http://fonts.googleapis.com/css?family=Open+Sans:300,600,400' rel='stylesheet' type='text/css'>
+
+    <link rel="stylesheet" type="text/css" href="../css/jsgrid.css" />
+    <link rel="stylesheet" type="text/css" href="../css/theme.css" />
+
+    <script src="../external/jquery/jquery-1.8.3.js"></script>
+    <script src="db.js"></script>
+
+    <script src="../src/jsgrid.core.js"></script>
+    <script src="../src/jsgrid.load-indicator.js"></script>
+    <script src="../src/jsgrid.load-strategies.js"></script>
+    <script src="../src/jsgrid.sort-strategies.js"></script>
+    <script src="../src/jsgrid.field.js"></script>
+    <script src="../src/fields/jsgrid.field.text.js"></script>
+    <script src="../src/fields/jsgrid.field.number.js"></script>
+    <script src="../src/fields/jsgrid.field.select.js"></script>
+    <script src="../src/fields/jsgrid.field.checkbox.js"></script>
+</head>
+<body>
+    <h1>Static Data</h1>
+    <div id="jsGrid"></div>
+
+    <script>
+        $(function() {
+
+            $("#jsGrid").jsGrid({
+                height: "70%",
+                width: "100%",
+                sorting: true,
+                paging: true,
+                fields: [
+                    { name: "Name", type: "text", width: 150 },
+                    { name: "Age", type: "number", width: 50 },
+                    { name: "Address", type: "text", width: 200 },
+                    { name: "Country", type: "select", items: db.countries, valueField: "Id", textField: "Name" },
+                    { name: "Married", type: "checkbox", title: "Is Married" }
+                ],
+                data: db.clients
+            });
+
+        });
+    </script>
+</body>
+</html>

+ 61 - 0
src/api/static/js/jsgrid/demos/validation.html

@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <title>jsGrid - Validation</title>
+    <link rel="stylesheet" type="text/css" href="demos.css" />
+    <link href='http://fonts.googleapis.com/css?family=Open+Sans:300,600,400' rel='stylesheet' type='text/css'>
+
+    <link rel="stylesheet" type="text/css" href="../css/jsgrid.css" />
+    <link rel="stylesheet" type="text/css" href="../css/theme.css" />
+
+    <script src="../external/jquery/jquery-1.8.3.js"></script>
+    <script src="db.js"></script>
+
+    <script src="../src/jsgrid.core.js"></script>
+    <script src="../src/jsgrid.load-indicator.js"></script>
+    <script src="../src/jsgrid.load-strategies.js"></script>
+    <script src="../src/jsgrid.sort-strategies.js"></script>
+    <script src="../src/jsgrid.validation.js"></script>
+    <script src="../src/jsgrid.field.js"></script>
+    <script src="../src/fields/jsgrid.field.text.js"></script>
+    <script src="../src/fields/jsgrid.field.number.js"></script>
+    <script src="../src/fields/jsgrid.field.select.js"></script>
+    <script src="../src/fields/jsgrid.field.checkbox.js"></script>
+    <script src="../src/fields/jsgrid.field.control.js"></script>
+</head>
+<body>
+    <h1>Validation</h1>
+    <div id="jsGrid"></div>
+
+    <script>
+        $(function() {
+
+            $("#jsGrid").jsGrid({
+                height: "70%",
+                width: "100%",
+                filtering: true,
+                editing: true,
+                inserting: true,
+                sorting: true,
+                paging: true,
+                autoload: true,
+                pageSize: 15,
+                pageButtonCount: 5,
+                deleteConfirm: "Do you really want to delete the client?",
+                controller: db,
+                fields: [
+                    { name: "Name", type: "text", width: 150, validate: "required" },
+                    { name: "Age", type: "number", width: 50, validate: { validator: "range", param: [18,80] } },
+                    { name: "Address", type: "text", width: 200, validate: { validator: "rangeLength", param: [10, 250] } },
+                    { name: "Country", type: "select", items: db.countries, valueField: "Id", textField: "Name",
+                        validate: { message: "Country should be specified", validator: function(value) { return value > 0; } } },
+                    { name: "Married", type: "checkbox", title: "Is Married", sorting: false },
+                    { type: "control" }
+                ]
+            });
+
+        });
+    </script>
+</body>
+</html>

+ 235 - 0
src/api/static/js/jsgrid/external/qunit/qunit-1.10.0.css

@@ -0,0 +1,235 @@
+/**
+ * QUnit v1.10.0 - A JavaScript Unit Testing Framework
+ *
+ * http://qunitjs.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ */
+
+/** Font Family and Sizes */
+
+#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
+	font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
+}
+
+#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
+#qunit-tests { font-size: smaller; }
+
+
+/** Resets */
+
+#qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
+	margin: 0;
+	padding: 0;
+}
+
+
+/** Header */
+
+#qunit-header {
+	padding: 0.5em 0 0.5em 1em;
+
+	color: #8699a4;
+	background-color: #0d3349;
+
+	font-size: 1.5em;
+	line-height: 1em;
+	font-weight: normal;
+
+	border-radius: 5px 5px 0 0;
+	-moz-border-radius: 5px 5px 0 0;
+	-webkit-border-top-right-radius: 5px;
+	-webkit-border-top-left-radius: 5px;
+}
+
+#qunit-header a {
+	text-decoration: none;
+	color: #c2ccd1;
+}
+
+#qunit-header a:hover,
+#qunit-header a:focus {
+	color: #fff;
+}
+
+#qunit-testrunner-toolbar label {
+	display: inline-block;
+	padding: 0 .5em 0 .1em;
+}
+
+#qunit-banner {
+	height: 5px;
+}
+
+#qunit-testrunner-toolbar {
+	padding: 0.5em 0 0.5em 2em;
+	color: #5E740B;
+	background-color: #eee;
+	overflow: hidden;
+}
+
+#qunit-userAgent {
+	padding: 0.5em 0 0.5em 2.5em;
+	background-color: #2b81af;
+	color: #fff;
+	text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
+}
+
+#qunit-modulefilter-container {
+	float: right;
+}
+
+/** Tests: Pass/Fail */
+
+#qunit-tests {
+	list-style-position: inside;
+}
+
+#qunit-tests li {
+	padding: 0.4em 0.5em 0.4em 2.5em;
+	border-bottom: 1px solid #fff;
+	list-style-position: inside;
+}
+
+#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running  {
+	display: none;
+}
+
+#qunit-tests li strong {
+	cursor: pointer;
+}
+
+#qunit-tests li a {
+	padding: 0.5em;
+	color: #c2ccd1;
+	text-decoration: none;
+}
+#qunit-tests li a:hover,
+#qunit-tests li a:focus {
+	color: #000;
+}
+
+#qunit-tests ol {
+	margin-top: 0.5em;
+	padding: 0.5em;
+
+	background-color: #fff;
+
+	border-radius: 5px;
+	-moz-border-radius: 5px;
+	-webkit-border-radius: 5px;
+}
+
+#qunit-tests table {
+	border-collapse: collapse;
+	margin-top: .2em;
+}
+
+#qunit-tests th {
+	text-align: right;
+	vertical-align: top;
+	padding: 0 .5em 0 0;
+}
+
+#qunit-tests td {
+	vertical-align: top;
+}
+
+#qunit-tests pre {
+	margin: 0;
+	white-space: pre-wrap;
+	word-wrap: break-word;
+}
+
+#qunit-tests del {
+	background-color: #e0f2be;
+	color: #374e0c;
+	text-decoration: none;
+}
+
+#qunit-tests ins {
+	background-color: #ffcaca;
+	color: #500;
+	text-decoration: none;
+}
+
+/*** Test Counts */
+
+#qunit-tests b.counts                       { color: black; }
+#qunit-tests b.passed                       { color: #5E740B; }
+#qunit-tests b.failed                       { color: #710909; }
+
+#qunit-tests li li {
+	padding: 5px;
+	background-color: #fff;
+	border-bottom: none;
+	list-style-position: inside;
+}
+
+/*** Passing Styles */
+
+#qunit-tests li li.pass {
+	color: #3c510c;
+	background-color: #fff;
+	border-left: 10px solid #C6E746;
+}
+
+#qunit-tests .pass                          { color: #528CE0; background-color: #D2E0E6; }
+#qunit-tests .pass .test-name               { color: #366097; }
+
+#qunit-tests .pass .test-actual,
+#qunit-tests .pass .test-expected           { color: #999999; }
+
+#qunit-banner.qunit-pass                    { background-color: #C6E746; }
+
+/*** Failing Styles */
+
+#qunit-tests li li.fail {
+	color: #710909;
+	background-color: #fff;
+	border-left: 10px solid #EE5757;
+	white-space: pre;
+}
+
+#qunit-tests > li:last-child {
+	border-radius: 0 0 5px 5px;
+	-moz-border-radius: 0 0 5px 5px;
+	-webkit-border-bottom-right-radius: 5px;
+	-webkit-border-bottom-left-radius: 5px;
+}
+
+#qunit-tests .fail                          { color: #000000; background-color: #EE5757; }
+#qunit-tests .fail .test-name,
+#qunit-tests .fail .module-name             { color: #000000; }
+
+#qunit-tests .fail .test-actual             { color: #EE5757; }
+#qunit-tests .fail .test-expected           { color: green;   }
+
+#qunit-banner.qunit-fail                    { background-color: #EE5757; }
+
+
+/** Result */
+
+#qunit-testresult {
+	padding: 0.5em 0.5em 0.5em 2.5em;
+
+	color: #2b81af;
+	background-color: #D2E0E6;
+
+	border-bottom: 1px solid white;
+}
+#qunit-testresult .module-name {
+	font-weight: bold;
+}
+
+/** Fixture */
+
+#qunit-fixture {
+	position: absolute;
+	top: -10000px;
+	left: -10000px;
+	width: 1000px;
+	height: 1000px;
+}

Разница между файлами не показана из-за своего большого размера
+ 1977 - 0
src/api/static/js/jsgrid/external/qunit/qunit-1.10.0.js


Разница между файлами не показана из-за своего большого размера
+ 258 - 0
src/api/static/js/jsgrid/jsgrid-theme.css


Разница между файлами не показана из-за своего большого размера
+ 7 - 0
src/api/static/js/jsgrid/jsgrid-theme.min.css


+ 126 - 0
src/api/static/js/jsgrid/jsgrid.css

@@ -0,0 +1,126 @@
+/*
+ * jsGrid v1.5.3 (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-cell {
+    padding: 0.5em 0.5em;
+}
+
+.jsgrid-сell,
+.jsgrid-header-cell {
+    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-cell {
+    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 .jsgrid-cell {
+    cursor: pointer;
+}
+
+.jsgrid-nodata-row .jsgrid-cell {
+    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;
+}

Разница между файлами не показана из-за своего большого размера
+ 2516 - 0
src/api/static/js/jsgrid/jsgrid.js


Разница между файлами не показана из-за своего большого размера
+ 7 - 0
src/api/static/js/jsgrid/jsgrid.min.css


Разница между файлами не показана из-за своего большого размера
+ 8 - 0
src/api/static/js/jsgrid/jsgrid.min.js


+ 38 - 0
src/api/static/js/jsgrid/package.json

@@ -0,0 +1,38 @@
+{
+    "name": "jsgrid",
+    "version": "1.5.3",
+    "description": "Lightweight data grid jQuery plugin. It supports basic grid operations like inserting, filtering, editing, deleting, paging, sorting, and validation. jsGrid is tunable and allows to customize appearance and components.",
+    "keywords": [
+        "grid",
+        "jquery",
+        "plugin"
+    ],
+    "homepage": "http://js-grid.com",
+    "author": "Artem Tabalin",
+    "license": {
+        "type": "MIT",
+        "url": "https://github.com/tabalinas/jsgrid/blob/master/LICENSE"
+    },
+    "main": "dist/jsgrid.js",
+    "directories": {
+        "test": "tests"
+    },
+    "repository": {
+        "type": "git",
+        "url": "https://github.com/tabalinas/jsgrid"
+    },
+    "bugs": {
+        "url": "https://github.com/tabalinas/jsgrid/issues"
+    },
+    "dependencies": {},
+    "devDependencies": {
+        "grunt": "^0.4.5",
+        "grunt-contrib-concat": "~0.3.0",
+        "grunt-contrib-copy": "^0.7.0",
+        "grunt-contrib-cssmin": "^0.10.0",
+        "grunt-contrib-qunit": "^0.5.2",
+        "grunt-contrib-uglify": "^0.4.0",
+        "grunt-image-embed": "^0.3.1",
+        "grunt-string-replace": "^1.2.1"
+    }
+}

+ 97 - 0
src/api/static/js/jsgrid/src/fields/jsgrid.field.checkbox.js

@@ -0,0 +1,97 @@
+(function(jsGrid, $, undefined) {
+
+    var Field = jsGrid.Field;
+
+    function CheckboxField(config) {
+        Field.call(this, config);
+    }
+
+    CheckboxField.prototype = new Field({
+
+        sorter: "number",
+        align: "center",
+        autosearch: true,
+
+        itemTemplate: function(value) {
+            return this._createCheckbox().prop({
+                checked: value,
+                disabled: true
+            });
+        },
+
+        filterTemplate: function() {
+            if(!this.filtering)
+                return "";
+
+            var grid = this._grid,
+                $result = this.filterControl = this._createCheckbox();
+
+            $result.prop({
+                readOnly: true,
+                indeterminate: true
+            });
+
+            $result.on("click", function() {
+                var $cb = $(this);
+
+                if($cb.prop("readOnly")) {
+                    $cb.prop({
+                        checked: false,
+                        readOnly: false
+                    });
+                }
+                else if(!$cb.prop("checked")) {
+                    $cb.prop({
+                        readOnly: true,
+                        indeterminate: true
+                    });
+                }
+            });
+
+            if(this.autosearch) {
+                $result.on("click", function() {
+                    grid.search();
+                });
+            }
+
+            return $result;
+        },
+
+        insertTemplate: function() {
+            if(!this.inserting)
+                return "";
+
+            return this.insertControl = this._createCheckbox();
+        },
+
+        editTemplate: function(value) {
+            if(!this.editing)
+                return this.itemTemplate.apply(this, arguments);
+
+            var $result = this.editControl = this._createCheckbox();
+            $result.prop("checked", value);
+            return $result;
+        },
+
+        filterValue: function() {
+            return this.filterControl.get(0).indeterminate
+                ? undefined
+                : this.filterControl.is(":checked");
+        },
+
+        insertValue: function() {
+            return this.insertControl.is(":checked");
+        },
+
+        editValue: function() {
+            return this.editControl.is(":checked");
+        },
+
+        _createCheckbox: function() {
+            return $("<input>").attr("type", "checkbox");
+        }
+    });
+
+    jsGrid.fields.checkbox = jsGrid.CheckboxField = CheckboxField;
+
+}(jsGrid, jQuery));

+ 223 - 0
src/api/static/js/jsgrid/src/fields/jsgrid.field.control.js

@@ -0,0 +1,223 @@
+(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));

+ 41 - 0
src/api/static/js/jsgrid/src/fields/jsgrid.field.number.js

@@ -0,0 +1,41 @@
+(function(jsGrid, $, undefined) {
+
+    var TextField = jsGrid.TextField;
+
+    function NumberField(config) {
+        TextField.call(this, config);
+    }
+
+    NumberField.prototype = new TextField({
+
+        sorter: "number",
+        align: "right",
+		readOnly: false,
+
+        filterValue: function() {
+            return this.filterControl.val()
+                ? parseInt(this.filterControl.val() || 0, 10)
+                : undefined;
+        },
+
+        insertValue: function() {
+            return this.insertControl.val()
+                ? parseInt(this.insertControl.val() || 0, 10)
+                : undefined;
+        },
+
+        editValue: function() {
+            return this.editControl.val()
+                ? parseInt(this.editControl.val() || 0, 10)
+                : undefined;
+        },
+
+        _createTextBox: function() {
+			return $("<input>").attr("type", "number")
+                .prop("readonly", !!this.readOnly);
+        }
+    });
+
+    jsGrid.fields.number = jsGrid.NumberField = NumberField;
+
+}(jsGrid, jQuery));

+ 121 - 0
src/api/static/js/jsgrid/src/fields/jsgrid.field.select.js

@@ -0,0 +1,121 @@
+(function(jsGrid, $, undefined) {
+
+    var NumberField = jsGrid.NumberField;
+    var numberValueType = "number";
+    var stringValueType = "string";
+
+    function SelectField(config) {
+        this.items = [];
+        this.selectedIndex = -1;
+        this.valueField = "";
+        this.textField = "";
+
+        if(config.valueField && config.items.length) {
+            var firstItemValue = config.items[0][config.valueField];
+            this.valueType = (typeof firstItemValue) === numberValueType ? numberValueType : stringValueType;
+        }
+
+        this.sorter = this.valueType;
+
+        NumberField.call(this, config);
+    }
+
+    SelectField.prototype = new NumberField({
+
+        align: "center",
+        valueType: numberValueType,
+
+        itemTemplate: function(value) {
+            var items = this.items,
+                valueField = this.valueField,
+                textField = this.textField,
+                resultItem;
+
+            if(valueField) {
+                resultItem = $.grep(items, function(item, index) {
+                    return item[valueField] === value;
+                })[0] || {};
+            }
+            else {
+                resultItem = items[value];
+            }
+
+            var result = (textField ? resultItem[textField] : resultItem);
+
+            return (result === undefined || result === null) ? "" : result;
+        },
+
+        filterTemplate: function() {
+            if(!this.filtering)
+                return "";
+
+            var grid = this._grid,
+                $result = this.filterControl = this._createSelect();
+
+            if(this.autosearch) {
+                $result.on("change", function(e) {
+                    grid.search();
+                });
+            }
+
+            return $result;
+        },
+
+        insertTemplate: function() {
+            if(!this.inserting)
+                return "";
+
+            return this.insertControl = this._createSelect();
+        },
+
+        editTemplate: function(value) {
+            if(!this.editing)
+                return this.itemTemplate.apply(this, arguments);
+
+            var $result = this.editControl = this._createSelect();
+            (value !== undefined) && $result.val(value);
+            return $result;
+        },
+
+        filterValue: function() {
+            var val = this.filterControl.val();
+            return this.valueType === numberValueType ? parseInt(val || 0, 10) : val;
+        },
+
+        insertValue: function() {
+            var val = this.insertControl.val();
+            return this.valueType === numberValueType ? parseInt(val || 0, 10) : val;
+        },
+
+        editValue: function() {
+            var val = this.editControl.val();
+            return this.valueType === numberValueType ? parseInt(val || 0, 10) : val;
+        },
+
+        _createSelect: function() {
+            var $result = $("<select>"),
+                valueField = this.valueField,
+                textField = this.textField,
+                selectedIndex = this.selectedIndex;
+
+            $.each(this.items, function(index, item) {
+                var value = valueField ? item[valueField] : index,
+                    text = textField ? item[textField] : item;
+
+                var $option = $("<option>")
+                    .attr("value", value)
+                    .text(text)
+                    .appendTo($result);
+
+                $option.prop("selected", (selectedIndex === index));
+            });
+
+            $result.prop("disabled", !!this.readOnly);
+
+            return $result;
+        }
+    });
+
+    jsGrid.fields.select = jsGrid.SelectField = SelectField;
+
+}(jsGrid, jQuery));

+ 69 - 0
src/api/static/js/jsgrid/src/fields/jsgrid.field.text.js

@@ -0,0 +1,69 @@
+(function(jsGrid, $, undefined) {
+
+    var Field = jsGrid.Field;
+
+    function TextField(config) {
+        Field.call(this, config);
+    }
+
+    TextField.prototype = new Field({
+
+        autosearch: true,
+		readOnly: false,
+
+        filterTemplate: function() {
+            if(!this.filtering)
+                return "";
+
+            var grid = this._grid,
+                $result = this.filterControl = this._createTextBox();
+
+            if(this.autosearch) {
+                $result.on("keypress", function(e) {
+                    if(e.which === 13) {
+                        grid.search();
+                        e.preventDefault();
+                    }
+                });
+            }
+
+            return $result;
+        },
+
+        insertTemplate: function() {
+            if(!this.inserting)
+                return "";
+
+            return this.insertControl = this._createTextBox();
+        },
+
+        editTemplate: function(value) {
+            if(!this.editing)
+                return this.itemTemplate.apply(this, arguments);
+
+            var $result = this.editControl = this._createTextBox();
+            $result.val(value);
+            return $result;
+        },
+
+        filterValue: function() {
+            return this.filterControl.val();
+        },
+
+        insertValue: function() {
+            return this.insertControl.val();
+        },
+
+        editValue: function() {
+            return this.editControl.val();
+        },
+
+        _createTextBox: function() {
+            return $("<input>").attr("type", "text")
+                .prop("readonly", !!this.readOnly);
+        }
+    });
+
+    jsGrid.fields.text = jsGrid.TextField = TextField;
+
+}(jsGrid, jQuery));

+ 34 - 0
src/api/static/js/jsgrid/src/fields/jsgrid.field.textarea.js

@@ -0,0 +1,34 @@
+(function(jsGrid, $, undefined) {
+
+    var TextField = jsGrid.TextField;
+
+    function TextAreaField(config) {
+        TextField.call(this, config);
+    }
+
+    TextAreaField.prototype = new TextField({
+
+        insertTemplate: function() {
+            if(!this.inserting)
+                return "";
+
+            return this.insertControl = this._createTextArea();
+        },
+
+        editTemplate: function(value) {
+            if(!this.editing)
+                return this.itemTemplate.apply(this, arguments);
+
+            var $result = this.editControl = this._createTextArea();
+            $result.val(value);
+            return $result;
+        },
+
+        _createTextArea: function() {
+            return $("<textarea>").prop("readonly", !!this.readOnly);
+        }
+    });
+
+    jsGrid.fields.textarea = jsGrid.TextAreaField = TextAreaField;
+
+}(jsGrid, jQuery));

+ 46 - 0
src/api/static/js/jsgrid/src/i18n/de.js

@@ -0,0 +1,46 @@
+(function(jsGrid) {
+
+    jsGrid.locales.de = {
+        grid: {
+            noDataContent: "Die Daten konnten nicht gefunden werden",
+            deleteConfirm: "Möchten Sie die Daten unwiederruflich löschen?",
+            pagerFormat: "Seiten: {first} {prev} {pages} {next} {last} &nbsp;&nbsp; {pageIndex} von {pageCount}",
+            pagePrevText: "<",
+            pageNextText: ">",
+            pageFirstText: "<<",
+            pageLastText: ">>",
+            loadMessage: "Bitte warten...",
+            invalidMessage: "Ihre Eingabe ist nicht zulässig!"
+        },
+
+        loadIndicator: {
+            message: "Lädt..."
+        },
+
+        fields: {
+            control: {
+                searchModeButtonTooltip: "Suche",
+                insertModeButtonTooltip: "Eintrag hinzufügen",
+                editButtonTooltip: "Bearbeiten",
+                deleteButtonTooltip: "Löschen",
+                searchButtonTooltip: "Eintrag finden",
+                clearFilterButtonTooltip: "Filter zurücksetzen",
+                insertButtonTooltip: "Hinzufügen",
+                updateButtonTooltip: "Speichern",
+                cancelEditButtonTooltip: "Abbrechen"
+            }
+        },
+
+        validators: {
+            required: { message: "Dies ist ein Pflichtfeld" },
+            rangeLength: { message: "Die Länge der Eingabe liegt außerhalb des zulässigen Bereichs" },
+            minLength: { message: "Die Eingabe ist zu kurz" },
+            maxLength: { message: "Die Eingabe ist zu lang" },
+            pattern: { message: "Die Eingabe entspricht nicht dem gewünschten Muster" },
+            range: { message: "Der eingegebene Wert liegt außerhalb des zulässigen Bereichs" },
+            min: { message: "Der eingegebene Wert ist zu niedrig" },
+            max: { message: "Der eingegebene Wert ist zu hoch" }
+        }
+    };
+
+}(jsGrid, jQuery));

+ 46 - 0
src/api/static/js/jsgrid/src/i18n/es.js

@@ -0,0 +1,46 @@
+(function(jsGrid) {
+
+    jsGrid.locales.es = {
+        grid: {
+            noDataContent: "No encontrado",
+            deleteConfirm: "¿Está seguro?",
+            pagerFormat: "Paginas: {first} {prev} {pages} {next} {last} &nbsp;&nbsp; {pageIndex} de {pageCount}",
+            pagePrevText: "Anterior",
+            pageNextText: "Siguiente",
+            pageFirstText: "Primero",
+            pageLastText: "Ultimo",
+            loadMessage: "Por favor, espere...",
+            invalidMessage: "¡Datos no válidos!"
+        },
+
+        loadIndicator: {
+            message: "Cargando..."
+        },
+
+        fields: {
+            control: {
+                searchModeButtonTooltip: "Cambiar a búsqueda",
+                insertModeButtonTooltip: "Cambiar a inserción",
+                editButtonTooltip: "Editar",
+                deleteButtonTooltip: "Suprimir",
+                searchButtonTooltip: "Buscar",
+                clearFilterButtonTooltip: "Borrar filtro",
+                insertButtonTooltip: "Insertar",
+                updateButtonTooltip: "Actualizar",
+                cancelEditButtonTooltip: "Cancelar edición"
+            }
+        },
+
+        validators: {
+            required: { message: "Campo requerido" },
+            rangeLength: { message: "La longitud del valor está fuera del intervalo definido" },
+            minLength: { message: "La longitud del valor es demasiado corta" },
+            maxLength: { message: "La longitud del valor es demasiado larga" },
+            pattern: { message: "El valor no se ajusta al patrón definido" },
+            range: { message: "Valor fuera del rango definido" },
+            min: { message: "Valor demasiado bajo" },
+            max: { message: "Valor demasiado alto" }
+        }
+    };
+
+}(jsGrid, jQuery));

+ 47 - 0
src/api/static/js/jsgrid/src/i18n/fr.js

@@ -0,0 +1,47 @@
+(function(jsGrid) {
+
+    jsGrid.locales.fr = {
+        grid: {
+            noDataContent: "Pas de données",
+            deleteConfirm: "Êtes-vous sûr ?",
+            pagerFormat: "Pages: {first} {prev} {pages} {next} {last} &nbsp;&nbsp; {pageIndex} de {pageCount}",
+            pagePrevText: "<",
+            pageNextText: ">",
+            pageFirstText: "<<",
+            pageLastText: ">>",
+            loadMessage: "Chargement en cours...",
+            invalidMessage: "Des données incorrectes sont entrés !"
+        },
+
+        loadIndicator: {
+            message: "Chargement en cours..."
+        },
+
+        fields: {
+            control: {
+                searchModeButtonTooltip: "Recherche",
+                insertModeButtonTooltip: "Ajouter une entrée",
+                editButtonTooltip: "Changer",
+                deleteButtonTooltip: "Effacer",
+                searchButtonTooltip: "Trouve",
+                clearFilterButtonTooltip: "Effacer",
+                insertButtonTooltip: "Ajouter",
+                updateButtonTooltip: "Sauvegarder",
+                cancelEditButtonTooltip: "Annuler"
+            }
+        },
+
+        validators: {
+            required: { message: "Champ requis" },
+            rangeLength: { message: "Longueur de la valeur du champ est hors de la plage définie" },
+            minLength: { message: "La valeur du champ est trop court" },
+            maxLength: { message: "La valeur du champ est trop long" },
+            pattern: { message: "La valeur du champ ne correspond pas à la configuration définie" },
+            range: { message: "La valeur du champ est hors de la plage définie" },
+            min: { message: "La valeur du champ est trop petit" },
+            max: { message: "La valeur du champ est trop grande" }
+        }
+    };
+
+}(jsGrid, jQuery));
+

+ 46 - 0
src/api/static/js/jsgrid/src/i18n/he.js

@@ -0,0 +1,46 @@
+(function(jsGrid) {
+
+    jsGrid.locales.he = {
+        grid: {
+            noDataContent: "לא נמצא",
+            deleteConfirm: "האם אתה בטוח?",
+            pagerFormat: "עמודים: {first} {prev} {pages} {next} {last} &nbsp;&nbsp; {pageIndex} מתוך {pageCount}",
+            pagePrevText: "הקודם",
+            pageNextText: "הבא",
+            pageFirstText: "ראשון",
+            pageLastText: "אחרון",
+            loadMessage: "אנא המתן ...",
+            invalidMessage: "נתונים לא חוקיים!"
+        },
+
+        loadIndicator: {
+            message: "טוען..."
+        },
+
+        fields: {
+            control: {
+                searchModeButtonTooltip: "ביצוע חיפוש",
+                insertModeButtonTooltip: "ביצוע עריכת שורה",
+                editButtonTooltip: "עריכה",
+                deleteButtonTooltip: "מחיקה",
+                searchButtonTooltip: "חיפוש",
+                clearFilterButtonTooltip: "ניקוי מסנן",
+                insertButtonTooltip: "הכנסה",
+                updateButtonTooltip: "עדכון",
+                cancelEditButtonTooltip: "ביטול עריכה"
+            }
+        },
+
+        validators: {
+            required: { message: "שדה נדרש" },
+            rangeLength: { message: "אורכו של הערך הוא מחוץ לטווח המוגדר" },
+            minLength: { message: "אורכו של הערך קצר מדי" },
+            maxLength: { message: "אורכו של הערך ארוך מדי" },
+            pattern: { message: "אורכו של הערך ארוך מדי" },
+            range: { message: "ערך מחוץ לטווח" },
+            min: { message: "ערך נמוך מדי" },
+            max: { message: "גבוה מדי" }
+        }
+    };
+
+}(jsGrid, jQuery));

+ 46 - 0
src/api/static/js/jsgrid/src/i18n/ja.js

@@ -0,0 +1,46 @@
+(function(jsGrid) {
+
+    jsGrid.locales.ja = {
+        grid: {
+            noDataContent: "データが見つかりません。",
+            deleteConfirm: "削除しますよろしですか。",
+            pagerFormat: "頁: {first} {prev} {pages} {next} {last} &nbsp;&nbsp; 【{pageIndex}/{pageCount}】",
+            pagePrevText: "前",
+            pageNextText: "次",
+            pageFirstText: "最初",
+            pageLastText: "最後",
+            loadMessage: "しばらくお待ちください…",
+            invalidMessage: "入力されたデータが不正です。"
+        },
+
+        loadIndicator: {
+            message: "処理中…"
+        },
+
+        fields: {
+            control: {
+                searchModeButtonTooltip: "検索モードへ",
+                insertModeButtonTooltip: "登録モードへ",
+                editButtonTooltip: "編集",
+                deleteButtonTooltip: "削除",
+                searchButtonTooltip: "フィルター",
+                clearFilterButtonTooltip: "クリア",
+                insertButtonTooltip: "登録",
+                updateButtonTooltip: "更新",
+                cancelEditButtonTooltip: "編集戻す"
+            }
+        },
+
+        validators: {
+            required: { message: "項目が必要です。" },
+            rangeLength: { message: "項目の桁数が範囲外です。" },
+            minLength: { message: "項目の桁数が超過しています。" },
+            maxLength: { message: "項目の桁数が不足しています。" },
+            pattern: { message: "項目の値がパターンに一致しません。" },
+            range: { message: "項目の値が範囲外です。" },
+            min: { message: "項目の値が超過しています。" },
+            max: { message: "項目の値が不足しています。" }
+        }
+    };
+
+}(jsGrid, jQuery));

+ 46 - 0
src/api/static/js/jsgrid/src/i18n/ka.js

@@ -0,0 +1,46 @@
+(function(jsGrid) {
+
+    jsGrid.locales.ka = {
+        grid: {
+            noDataContent: "მონაცემები ცარიელია.",
+            deleteConfirm: "ნამდვილად გსურთ ჩანაწერის წაშლა?",
+            pagerFormat: "გვერდები: {first} {prev} {pages} {next} {last} &nbsp;&nbsp; {pageIndex} - {pageCount} დან.",
+            pagePrevText: "<",
+            pageNextText: ">",
+            pageFirstText: "<<",
+            pageLastText: ">>",
+            loadMessage: "გთხოვთ დაიცადოთ...",
+            invalidMessage: "შეყვანილია არასწორი მონაცემები!"
+        },
+
+        loadIndicator: {
+            message: "მიმდინარეობს ჩატვირთვა..."
+        },
+
+        fields: {
+            control: {
+                searchModeButtonTooltip: "ძებნა",
+                insertModeButtonTooltip: "ჩანაწერის დამატება",
+                editButtonTooltip: "შესწორება",
+                deleteButtonTooltip: "წაშლა",
+                searchButtonTooltip: "ძებნა",
+                clearFilterButtonTooltip: "ფილტრის გასუფთავება",
+                insertButtonTooltip: "დამატება",
+                updateButtonTooltip: "შენახვა",
+                cancelEditButtonTooltip: "გაუქმება"
+            }
+        },
+
+        validators: {
+            required: { message: "ველი აუცილებელია შესავსებად." },
+            rangeLength: { message: "შეყვანილი ჩანაწერის ზომა არ ექვემდებარება დიაპაზონს." },
+            minLength: { message: "შეყვანილი ჩანაწერის ზომა საკმაოდ პატარა არის." },
+            maxLength: { message: "შეყვანილი ჩანაწერის ზომა საკმაოდ დიდი არის." },
+            pattern: { message: "შეყვანილი მნიშვნელობა არ ემთხვევა მითითებულ შაბლონს." },
+            range: { message: "შეყვანილი ინფორმაცია არ ჯდება დიაპაზონში." },
+            min: { message: "შეყვანილი ინფორმაციის ზომა საკმაოდ პატარა არის." },
+            max: { message: "შეყვანილი ინფორმაციის ზომა საკმაოდ დიდი არის." }
+        }
+    };
+
+}(jsGrid, jQuery));

+ 62 - 0
src/api/static/js/jsgrid/src/i18n/pl.js

@@ -0,0 +1,62 @@
+(function(jsGrid) {
+
+    jsGrid.locales.pl = {
+        grid: {
+            noDataContent: "Nie znaleziono",
+            deleteConfirm: "Czy jesteś pewien?",
+            pagerFormat: "Strony: {first} {prev} {pages} {next} {last} &nbsp;&nbsp; {pageIndex} z {pageCount}",
+            pagePrevText: "Poprzednia",
+            pageNextText: "Następna",
+            pageFirstText: "Pierwsza",
+            pageLastText: "Ostatnia",
+            loadMessage: "Proszę czekać...",
+            invalidMessage: "Wprowadzono nieprawidłowe dane!"
+        },
+
+        loadIndicator: {
+            message: "Ładowanie..."
+        },
+
+        fields: {
+            control: {
+                searchModeButtonTooltip: "Wyszukiwanie",
+                insertModeButtonTooltip: "Dodawanie",
+                editButtonTooltip: "Edytuj",
+                deleteButtonTooltip: "Usuń",
+                searchButtonTooltip: "Szukaj",
+                clearFilterButtonTooltip: "Wyczyść filtr",
+                insertButtonTooltip: "Dodaj",
+                updateButtonTooltip: "Aktualizuj",
+                cancelEditButtonTooltip: "Anuluj edytowanie"
+            }
+        },
+
+        validators: {
+            required: {
+                message: "Pole jest wymagane"
+            },
+            rangeLength: {
+                message: "Długość wartości pola znajduje się poza zdefiniowanym zakresem"
+            },
+            minLength: {
+                message: "Wartość pola jest zbyt krótka"
+            },
+            maxLength: {
+                message: "Wartość pola jest zbyt długa"
+            },
+            pattern: {
+                message: "Wartość pola nie zgadza się ze zdefiniowanym wzorem"
+            },
+            range: {
+                message: "Wartość pola znajduje się poza zdefiniowanym zakresem"
+            },
+            min: {
+                message: "Wartość pola jest zbyt mała"
+            },
+            max: {
+                message: "Wartość pola jest zbyt duża"
+            }
+        }
+    };
+
+}(jsGrid, jQuery));

+ 46 - 0
src/api/static/js/jsgrid/src/i18n/pt-br.js

@@ -0,0 +1,46 @@
+(function(jsGrid) {
+
+    jsGrid.locales["pt-br"] = {
+        grid: {
+            noDataContent: "Não encontrado",
+            deleteConfirm: "Você tem certeza que deseja remover este item?",
+            pagerFormat: "Páginas: {first} {prev} {pages} {next} {last} &nbsp;&nbsp; {pageIndex} de {pageCount}",
+            pagePrevText: "Anterior",
+            pageNextText: "Seguinte",
+            pageFirstText: "Primeira",
+            pageLastText: "Última",
+            loadMessage: "Por favor, espere...",
+            invalidMessage: "Dados inválidos!"
+        },
+
+        loadIndicator: {
+            message: "Carregando..."
+        },
+
+        fields: {
+            control: {
+                searchModeButtonTooltip: "Mudar para busca",
+                insertModeButtonTooltip: "Mudar para inserção",
+                editButtonTooltip: "Editar",
+                deleteButtonTooltip: "Remover",
+                searchButtonTooltip: "Buscar",
+                clearFilterButtonTooltip: "Remover filtro",
+                insertButtonTooltip: "Adicionar",
+                updateButtonTooltip: "Atualizar",
+                cancelEditButtonTooltip: "Cancelar Edição"
+            }
+        },
+
+        validators: {
+            required: { message: "Campo obrigatório" },
+            rangeLength: { message: "O valor esta fora do intervaldo definido" },
+            minLength: { message: "O comprimento do valor é muito curto" },
+            maxLength: { message: "O comprimento valor é muito longo" },
+            pattern: { message: "O valor informado não é compatível com o padrão" },
+            range: { message: "O valor informado esta fora do limite definido" },
+            min: { message: "O valor é muito curto" },
+            max: { message: "O valor é muito longo" }
+        }
+    };
+
+}(jsGrid, jQuery));

+ 46 - 0
src/api/static/js/jsgrid/src/i18n/pt.js

@@ -0,0 +1,46 @@
+(function(jsGrid) {
+
+    jsGrid.locales.pt = {
+        grid: {
+            noDataContent: "Não encontrado",
+            deleteConfirm: "Você tem certeza que deseja remover este item?",
+            pagerFormat: "Páginas: {first} {prev} {pages} {next} {last} &nbsp;&nbsp; {pageIndex} de {pageCount}",
+            pagePrevText: "Anterior",
+            pageNextText: "Seguinte",
+            pageFirstText: "Primeira",
+            pageLastText: "Última",
+            loadMessage: "Por favor, espere...",
+            invalidMessage: "Dados inválidos!"
+        },
+
+        loadIndicator: {
+            message: "Carregando..."
+        },
+
+        fields: {
+            control: {
+                searchModeButtonTooltip: "Mudar para busca",
+                insertModeButtonTooltip: "Mudar para inserção",
+                editButtonTooltip: "Editar",
+                deleteButtonTooltip: "Remover",
+                searchButtonTooltip: "Buscar",
+                clearFilterButtonTooltip: "Remover filtro",
+                insertButtonTooltip: "Adicionar",
+                updateButtonTooltip: "Atualizar",
+                cancelEditButtonTooltip: "Cancelar Edição"
+            }
+        },
+
+        validators: {
+            required: { message: "Campo obrigatório" },
+            rangeLength: { message: "O valor esta fora do intervaldo definido" },
+            minLength: { message: "O comprimento do valor é muito curto" },
+            maxLength: { message: "O comprimento valor é muito longo" },
+            pattern: { message: "O valor informado não é compatível com o padrão" },
+            range: { message: "O valor informado esta fora do limite definido" },
+            min: { message: "O valor é muito curto" },
+            max: { message: "O valor é muito longo" }
+        }
+    };
+
+}(jsGrid, jQuery));

+ 47 - 0
src/api/static/js/jsgrid/src/i18n/ru.js

@@ -0,0 +1,47 @@
+(function(jsGrid) {
+
+    jsGrid.locales.ru = {
+        grid: {
+            noDataContent: "Данных не найдено",
+            deleteConfirm: "Вы действительно хотите удалить запись?",
+            pagerFormat: "Страницы: {first} {prev} {pages} {next} {last} &nbsp;&nbsp; {pageIndex} из {pageCount}",
+            pagePrevText: "<",
+            pageNextText: ">",
+            pageFirstText: "<<",
+            pageLastText: ">>",
+            loadMessage: "Пожалуйста, подождите...",
+            invalidMessage: "Введены неверные данные!"
+        },
+
+        loadIndicator: {
+            message: "Загрузка..."
+        },
+
+        fields: {
+            control: {
+                searchModeButtonTooltip: "Поиск",
+                insertModeButtonTooltip: "Добавить запись",
+                editButtonTooltip: "Изменить",
+                deleteButtonTooltip: "Удалить",
+                searchButtonTooltip: "Найти",
+                clearFilterButtonTooltip: "Очистить фильтр",
+                insertButtonTooltip: "Добавить",
+                updateButtonTooltip: "Сохранить",
+                cancelEditButtonTooltip: "Отменить"
+            }
+        },
+
+        validators: {
+            required: { message: "Поле обязательно для заполения" },
+            rangeLength: { message: "Длинна введенного значения вне допустимого диапазона" },
+            minLength: { message: "Введенное значение слишком короткое" },
+            maxLength: { message: "Введенное значение слишком длинное" },
+            pattern: { message: "Введенное значение не соответствует заданному шаблону" },
+            range: { message: "Введенное значение вне допустимого диапазона" },
+            min: { message: "Введенное значение слишком маленькое" },
+            max: { message: "Введенное значение слишком большое" }
+        }
+    };
+
+}(jsGrid, jQuery));
+

+ 47 - 0
src/api/static/js/jsgrid/src/i18n/tr.js

@@ -0,0 +1,47 @@
+(function(jsGrid) {
+
+    jsGrid.locales.tr = {
+        grid: {
+            noDataContent: "Kayıt Bulunamadı",
+            deleteConfirm: "Emin misiniz ?",
+            pagerFormat: "Sayfalar: {first} {prev} {pages} {next} {last} &nbsp;&nbsp; {pageIndex} / {pageCount}",
+            pagePrevText: "<",
+            pageNextText: ">",
+            pageFirstText: "<<",
+            pageLastText: ">>",
+            loadMessage: "Lütfen bekleyiniz...",
+            invalidMessage: "Geçersiz veri girişi !"
+        },
+
+        loadIndicator: {
+            message: "Yükleniyor..."
+        },
+
+        fields: {
+            control: {
+                searchModeButtonTooltip: "Arama moduna geç",
+                insertModeButtonTooltip: "Yeni kayıt moduna geç",
+                editButtonTooltip: "Değiştir",
+                deleteButtonTooltip: "Sil",
+                searchButtonTooltip: "Bul",
+                clearFilterButtonTooltip: "Filtreyi temizle",
+                insertButtonTooltip: "Ekle",
+                updateButtonTooltip: "Güncelle",
+                cancelEditButtonTooltip: "Güncelleme iptali"
+            }
+        },
+
+        validators: {
+            required: { message: "Gerekli alandır" },
+            rangeLength: { message: "Alan değerinin uzunluğu tanımlanan aralık dışındadır" },
+            minLength: { message: "Alan değeri çok kısadır" },
+            maxLength: { message: "Alan değeri çok uzundur" },
+            pattern: { message: "Alan değeri tanımlanan şablon ile eşleşmiyor" },
+            range: { message: "Alan değeri tanımlı aralığın dışındadır" },
+            min: { message: "Alan değeri çok küçüktür" },
+            max: { message: "Alan değeri çok büyüktür" }
+        }
+    };
+
+}(jsGrid, jQuery));
+

+ 46 - 0
src/api/static/js/jsgrid/src/i18n/zh-cn.js

@@ -0,0 +1,46 @@
+(function(jsGrid) {
+
+    jsGrid.locales["zh-cn"] = {
+        grid: {
+            noDataContent: "暂无数据",
+            deleteConfirm: "确认删除?",
+            pagerFormat: "页码: {first} {prev} {pages} {next} {last} &nbsp;&nbsp; {pageIndex} / {pageCount}",
+            pagePrevText: "上一页",
+            pageNextText: "下一页",
+            pageFirstText: "第一页",
+            pageLastText: "最后页",
+            loadMessage: "请稍后...",
+            invalidMessage: "数据有误!"
+        },
+
+        loadIndicator: {
+            message: "载入中..."
+        },
+
+        fields: {
+            control: {
+                searchModeButtonTooltip: "切换为搜索",
+                insertModeButtonTooltip: "切换为新增",
+                editButtonTooltip: "编辑",
+                deleteButtonTooltip: "删除",
+                searchButtonTooltip: "搜索",
+                clearFilterButtonTooltip: "清空过滤",
+                insertButtonTooltip: "插入",
+                updateButtonTooltip: "更新",
+                cancelEditButtonTooltip: "取消编辑"
+            }
+        },
+
+        validators: {
+            required: { message: "字段必填" },
+            rangeLength: { message: "字段值长度超过定义范围" },
+            minLength: { message: "字段长度过短" },
+            maxLength: { message: "字段长度过长" },
+            pattern: { message: "字段值不符合定义规则" },
+            range: { message: "字段值超过定义范围" },
+            min: { message: "字段值太小" },
+            max: { message: "字段值太大" }
+        }
+    };
+
+}(jsGrid, jQuery));

+ 46 - 0
src/api/static/js/jsgrid/src/i18n/zh-tw.js

@@ -0,0 +1,46 @@
+(function(jsGrid) {
+
+    jsGrid.locales["zh-tw"] = {
+        grid: {
+            noDataContent: "暫無資料",
+            deleteConfirm: "確認刪除?",
+            pagerFormat: "頁碼: {first} {prev} {pages} {next} {last} &nbsp;&nbsp; {pageIndex} / {pageCount}",
+            pagePrevText: "上一頁",
+            pageNextText: "下一頁",
+            pageFirstText: "第一頁",
+            pageLastText: "最後一頁",
+            loadMessage: "請稍候...",
+            invalidMessage: "輸入資料不正確"
+        },
+
+        loadIndicator: {
+            message: "載入中..."
+        },
+
+        fields: {
+            control: {
+                searchModeButtonTooltip: "切換為搜尋",
+                insertModeButtonTooltip: "切換為新增",
+                editButtonTooltip: "編輯",
+                deleteButtonTooltip: "刪除",
+                searchButtonTooltip: "搜尋",
+                clearFilterButtonTooltip: "清除搜尋條件",
+                insertButtonTooltip: "新增",
+                updateButtonTooltip: "修改",
+                cancelEditButtonTooltip: "取消編輯"
+            }
+        },
+
+        validators: {
+            required: { message: "欄位必填" },
+            rangeLength: { message: "欄位字串長度超出範圍" },
+            minLength: { message: "欄位字串長度太短" },
+            maxLength: { message: "欄位字串長度太長" },
+            pattern: { message: "欄位字串不符合規則" },
+            range: { message: "欄位數值超出範圍" },
+            min: { message: "欄位數值太小" },
+            max: { message: "欄位數值太大" }
+        }
+    };
+
+}(jsGrid, jQuery));

Разница между файлами не показана из-за своего большого размера
+ 1467 - 0
src/api/static/js/jsgrid/src/jsgrid.core.js


+ 72 - 0
src/api/static/js/jsgrid/src/jsgrid.field.js

@@ -0,0 +1,72 @@
+(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));

+ 82 - 0
src/api/static/js/jsgrid/src/jsgrid.load-indicator.js

@@ -0,0 +1,82 @@
+(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));

+ 122 - 0
src/api/static/js/jsgrid/src/jsgrid.load-strategies.js

@@ -0,0 +1,122 @@
+(function(jsGrid, $, undefined) {
+
+    function DirectLoadingStrategy(grid) {
+        this._grid = grid;
+    }
+
+    DirectLoadingStrategy.prototype = {
+
+        firstDisplayIndex: function() {
+            var grid = this._grid;
+            return grid.option("paging") ? (grid.option("pageIndex") - 1) * grid.option("pageSize") : 0;
+        },
+
+        lastDisplayIndex: function() {
+            var grid = this._grid;
+            var itemsCount = grid.option("data").length;
+
+            return grid.option("paging")
+                ? Math.min(grid.option("pageIndex") * grid.option("pageSize"), itemsCount)
+                : itemsCount;
+        },
+
+        itemsCount: function() {
+            return this._grid.option("data").length;
+        },
+
+        openPage: function(index) {
+            this._grid.refresh();
+        },
+
+        loadParams: function() {
+            return {};
+        },
+
+        sort: function() {
+            this._grid._sortData();
+            this._grid.refresh();
+            return $.Deferred().resolve().promise();
+        },
+
+        reset: function() {
+            this._grid.refresh();
+            return $.Deferred().resolve().promise();
+        },
+
+        finishLoad: function(loadedData) {
+            this._grid.option("data", loadedData);
+        },
+
+        finishInsert: function(insertedItem) {
+            var grid = this._grid;
+            grid.option("data").push(insertedItem);
+            grid.refresh();
+        },
+
+        finishDelete: function(deletedItem, deletedItemIndex) {
+            var grid = this._grid;
+            grid.option("data").splice(deletedItemIndex, 1);
+            grid.reset();
+        }
+    };
+
+
+    function PageLoadingStrategy(grid) {
+        this._grid = grid;
+        this._itemsCount = 0;
+    }
+
+    PageLoadingStrategy.prototype = {
+
+        firstDisplayIndex: function() {
+            return 0;
+        },
+
+        lastDisplayIndex: function() {
+            return this._grid.option("data").length;
+        },
+
+        itemsCount: function() {
+            return this._itemsCount;
+        },
+
+        openPage: function(index) {
+            this._grid.loadData();
+        },
+
+        loadParams: function() {
+            var grid = this._grid;
+            return {
+                pageIndex: grid.option("pageIndex"),
+                pageSize: grid.option("pageSize")
+            };
+        },
+
+        reset: function() {
+            return this._grid.loadData();
+        },
+
+        sort: function() {
+            return this._grid.loadData();
+        },
+
+        finishLoad: function(loadedData) {
+            this._itemsCount = loadedData.itemsCount;
+            this._grid.option("data", loadedData.data);
+        },
+
+        finishInsert: function(insertedItem) {
+            this._grid.search();
+        },
+
+        finishDelete: function(deletedItem, deletedItemIndex) {
+            this._grid.search();
+        }
+    };
+
+    jsGrid.loadStrategies = {
+        DirectLoadingStrategy: DirectLoadingStrategy,
+        PageLoadingStrategy: PageLoadingStrategy
+    };
+
+}(jsGrid, jQuery));

+ 36 - 0
src/api/static/js/jsgrid/src/jsgrid.sort-strategies.js

@@ -0,0 +1,36 @@
+(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));

+ 135 - 0
src/api/static/js/jsgrid/src/jsgrid.validation.js

@@ -0,0 +1,135 @@
+(function(jsGrid, $, undefined) {
+
+    function Validation(config) {
+        this._init(config);
+    }
+
+    Validation.prototype = {
+
+        _init: function(config) {
+            $.extend(true, this, config);
+        },
+
+        validate: function(args) {
+            var errors = [];
+
+            $.each(this._normalizeRules(args.rules), function(_, rule) {
+                if(rule.validator(args.value, args.item, rule.param))
+                    return;
+
+                var errorMessage = $.isFunction(rule.message) ? rule.message(args.value, args.item) : rule.message;
+                errors.push(errorMessage);
+            });
+
+            return errors;
+        },
+
+        _normalizeRules: function(rules) {
+            if(!$.isArray(rules))
+                rules = [rules];
+
+            return $.map(rules, $.proxy(function(rule) {
+                return this._normalizeRule(rule);
+            }, this));
+        },
+
+        _normalizeRule: function(rule) {
+            if(typeof rule === "string")
+                rule = { validator: rule };
+
+            if($.isFunction(rule))
+                rule = { validator: rule };
+
+            if($.isPlainObject(rule))
+                rule = $.extend({}, rule);
+            else
+                throw Error("wrong validation config specified");
+
+            if($.isFunction(rule.validator))
+                return rule;
+
+            return this._applyNamedValidator(rule, rule.validator);
+        },
+
+        _applyNamedValidator: function(rule, validatorName) {
+            delete rule.validator;
+
+            var validator = validators[validatorName];
+            if(!validator)
+                throw Error("unknown validator \"" + validatorName + "\"");
+
+            if($.isFunction(validator)) {
+                validator = { validator: validator };
+            }
+
+            return $.extend({}, validator, rule);
+        }
+    };
+
+    jsGrid.Validation = Validation;
+
+
+    var validators = {
+        required: {
+            message: "Field is required",
+            validator: function(value) {
+                return value !== undefined && value !== null && value !== "";
+            }
+        },
+
+        rangeLength: {
+            message: "Field value length is out of the defined range",
+            validator: function(value, _, param) {
+                return value.length >= param[0] && value.length <= param[1];
+            }
+        },
+
+        minLength: {
+            message: "Field value is too short",
+            validator: function(value, _, param) {
+                return value.length >= param;
+            }
+        },
+
+        maxLength: {
+            message: "Field value is too long",
+            validator: function(value, _, param) {
+                return value.length <= param;
+            }
+        },
+
+        pattern: {
+            message: "Field value is not matching the defined pattern",
+            validator: function(value, _, param) {
+                if(typeof param === "string") {
+                    param = new RegExp("^(?:" + param + ")$");
+                }
+                return param.test(value);
+            }
+        },
+
+        range: {
+            message: "Field value is out of the defined range",
+            validator: function(value, _, param) {
+                return value >= param[0] && value <= param[1];
+            }
+        },
+
+        min: {
+            message: "Field value is too small",
+            validator: function(value, _, param) {
+                return value >= param;
+            }
+        },
+
+        max: {
+            message: "Field value is too large",
+            validator: function(value, _, param) {
+                return value <= param;
+            }
+        }
+    };
+
+    jsGrid.validators = validators;
+
+}(jsGrid, jQuery));

+ 37 - 0
src/api/static/js/jsgrid/tests/index.html

@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <title>JSGrid QUnit Tests</title>
+    <link rel="stylesheet" href="../external/qunit/qunit-1.10.0.css">
+</head>
+<body>
+    <div id="qunit"></div>
+
+    <div id="qunit-fixture">
+        <div id="jsGrid"></div>
+    </div>
+
+    <script src="../external/qunit/qunit-1.10.0.js"></script>
+
+    <script src="../external/jquery/jquery-1.8.3.js"></script>
+
+    <script src="../src/jsgrid.core.js"></script>
+    <script src="../src/jsgrid.load-indicator.js"></script>
+    <script src="../src/jsgrid.load-strategies.js"></script>
+    <script src="../src/jsgrid.sort-strategies.js"></script>
+    <script src="../src/jsgrid.validation.js"></script>
+    <script src="../src/jsgrid.field.js"></script>
+    <script src="../src/fields/jsgrid.field.text.js"></script>
+    <script src="../src/fields/jsgrid.field.number.js"></script>
+    <script src="../src/fields/jsgrid.field.textarea.js"></script>
+    <script src="../src/fields/jsgrid.field.checkbox.js"></script>
+    <script src="../src/fields/jsgrid.field.select.js"></script>
+    <script src="../src/fields/jsgrid.field.control.js"></script>
+
+    <script src="jsgrid.tests.js"></script>
+    <script src="jsgrid.field.tests.js"></script>
+    <script src="jsgrid.sort-strategies.tests.js"></script>
+    <script src="jsgrid.validation.tests.js"></script>
+</body>
+</html>

+ 461 - 0
src/api/static/js/jsgrid/tests/jsgrid.field.tests.js

@@ -0,0 +1,461 @@
+$(function() {
+
+    var Grid = jsGrid.Grid;
+
+    module("common field config", {
+        setup: function() {
+            this.isFieldExcluded = function(FieldClass) {
+                return FieldClass === jsGrid.ControlField;
+            };
+        }
+    });
+
+    test("filtering=false prevents rendering filter template", function() {
+        var isFieldExcluded = this.isFieldExcluded;
+
+        $.each(jsGrid.fields, function(name, FieldClass) {
+            if(isFieldExcluded(FieldClass))
+                return;
+
+            var field = new FieldClass({ filtering: false });
+
+            equal(field.filterTemplate(), "", "empty filter template for field " + name);
+        });
+    });
+
+    test("inserting=false prevents rendering insert template", function() {
+        var isFieldExcluded = this.isFieldExcluded;
+
+        $.each(jsGrid.fields, function(name, FieldClass) {
+            if(isFieldExcluded(FieldClass))
+                return;
+
+            var field = new FieldClass({ inserting: false });
+
+            equal(field.insertTemplate(), "", "empty insert template for field " + name);
+        });
+    });
+
+    test("editing=false renders itemTemplate", function() {
+        var isFieldExcluded = this.isFieldExcluded;
+
+        $.each(jsGrid.fields, function(name, FieldClass) {
+            if(isFieldExcluded(FieldClass))
+                return;
+
+            var item = {
+                field: "test"
+            };
+            var args;
+
+            var field = new FieldClass({
+                editing: false,
+                itemTemplate: function() {
+                    args = arguments;
+                    FieldClass.prototype.itemTemplate.apply(this, arguments);
+                }
+            });
+
+            var itemTemplate = field.itemTemplate("test", item);
+            var editTemplate = field.editTemplate("test", item);
+
+            var editTemplateContent = editTemplate instanceof jQuery ? editTemplate[0].outerHTML : editTemplate;
+            var itemTemplateContent = itemTemplate instanceof jQuery ? itemTemplate[0].outerHTML : itemTemplate;
+
+            equal(editTemplateContent, itemTemplateContent, "item template is rendered instead of edit template for " + name);
+            equal(args.length, 2, "passed both arguments for " + name);
+            equal(args[0], "test", "field value passed as a first argument for " + name);
+            equal(args[1], item, "item passed as a second argument for " + name);
+        });
+    });
+
+    module("jsGrid.field");
+
+    test("basic", function() {
+        var customSortingFunc = function() {
+                return 1;
+            },
+            field = new jsGrid.Field({
+                name: "testField",
+                title: "testTitle",
+                sorter: customSortingFunc
+            });
+
+        equal(field.headerTemplate(), "testTitle");
+        equal(field.itemTemplate("testValue"), "testValue");
+        equal(field.filterTemplate(), "");
+        equal(field.insertTemplate(), "");
+        equal(field.editTemplate("testValue"), "testValue");
+        strictEqual(field.filterValue(), "");
+        strictEqual(field.insertValue(), "");
+        strictEqual(field.editValue(), "testValue");
+        strictEqual(field.sortingFunc, customSortingFunc);
+    });
+
+
+    module("jsGrid.field.text");
+
+    test("basic", function() {
+        var field = new jsGrid.TextField({ name: "testField" });
+
+        equal(field.itemTemplate("testValue"), "testValue");
+        equal(field.filterTemplate()[0].tagName.toLowerCase(), "input");
+        equal(field.insertTemplate()[0].tagName.toLowerCase(), "input");
+        equal(field.editTemplate("testEditValue")[0].tagName.toLowerCase(), "input");
+        strictEqual(field.filterValue(), "");
+        strictEqual(field.insertValue(), "");
+        strictEqual(field.editValue(), "testEditValue");
+    });
+
+    test("set default field options with setDefaults", function() {
+        jsGrid.setDefaults("text", {
+            defaultOption: "test"
+        });
+
+        var $element = $("#jsGrid").jsGrid({
+            fields: [{ type: "text" }]
+        });
+
+        equal($element.jsGrid("option", "fields")[0].defaultOption, "test", "default field option set");
+    });
+
+
+    module("jsGrid.field.number");
+
+    test("basic", function() {
+        var field = new jsGrid.NumberField({ name: "testField" });
+
+        equal(field.itemTemplate(5), "5");
+        equal(field.filterTemplate()[0].tagName.toLowerCase(), "input");
+        equal(field.insertTemplate()[0].tagName.toLowerCase(), "input");
+        equal(field.editTemplate(6)[0].tagName.toLowerCase(), "input");
+        strictEqual(field.filterValue(), undefined);
+        strictEqual(field.insertValue(), undefined);
+        strictEqual(field.editValue(), 6);
+    });
+
+
+    module("jsGrid.field.textArea");
+
+    test("basic", function() {
+        var field = new jsGrid.TextAreaField({ name: "testField" });
+
+        equal(field.itemTemplate("testValue"), "testValue");
+        equal(field.filterTemplate()[0].tagName.toLowerCase(), "input");
+        equal(field.insertTemplate()[0].tagName.toLowerCase(), "textarea");
+        equal(field.editTemplate("testEditValue")[0].tagName.toLowerCase(), "textarea");
+        strictEqual(field.insertValue(), "");
+        strictEqual(field.editValue(), "testEditValue");
+    });
+
+
+    module("jsGrid.field.checkbox");
+
+    test("basic", function() {
+        var field = new jsGrid.CheckboxField({ name: "testField" }),
+            itemTemplate,
+            filterTemplate,
+            insertTemplate,
+            editTemplate;
+
+        itemTemplate = field.itemTemplate("testValue");
+        equal(itemTemplate[0].tagName.toLowerCase(), "input");
+        equal(itemTemplate.attr("type"), "checkbox");
+        equal(itemTemplate.attr("disabled"), "disabled");
+
+        filterTemplate = field.filterTemplate();
+        equal(filterTemplate[0].tagName.toLowerCase(), "input");
+        equal(filterTemplate.attr("type"), "checkbox");
+        equal(filterTemplate.prop("indeterminate"), true);
+
+        insertTemplate = field.insertTemplate();
+        equal(insertTemplate[0].tagName.toLowerCase(), "input");
+        equal(insertTemplate.attr("type"), "checkbox");
+
+        editTemplate = field.editTemplate(true);
+        equal(editTemplate[0].tagName.toLowerCase(), "input");
+        equal(editTemplate.attr("type"), "checkbox");
+        equal(editTemplate.is(":checked"), true);
+
+        strictEqual(field.filterValue(), undefined);
+        strictEqual(field.insertValue(), false);
+        strictEqual(field.editValue(), true);
+    });
+
+
+    module("jsGrid.field.select");
+
+    test("basic", function() {
+        var field,
+            filterTemplate,
+            insertTemplate,
+            editTemplate;
+
+        field = new jsGrid.SelectField({
+            name: "testField",
+            items: ["test1", "test2", "test3"],
+            selectedIndex: 1
+        });
+
+        equal(field.itemTemplate(1), "test2");
+
+        filterTemplate = field.filterTemplate();
+        equal(filterTemplate[0].tagName.toLowerCase(), "select");
+        equal(filterTemplate.children().length, 3);
+
+        insertTemplate = field.insertTemplate();
+        equal(insertTemplate[0].tagName.toLowerCase(), "select");
+        equal(insertTemplate.children().length, 3);
+
+        editTemplate = field.editTemplate(2);
+        equal(editTemplate[0].tagName.toLowerCase(), "select");
+        equal(editTemplate.find("option:selected").length, 1);
+        ok(editTemplate.children().eq(2).is(":selected"));
+
+        strictEqual(field.filterValue(), 1);
+        strictEqual(field.insertValue(), 1);
+        strictEqual(field.editValue(), 2);
+    });
+
+    test("items as array of integers", function() {
+        var field,
+            filterTemplate,
+            insertTemplate,
+            editTemplate;
+
+        field = new jsGrid.SelectField({
+            name: "testField",
+            items: [0, 10, 20],
+            selectedIndex: 0
+        });
+
+        strictEqual(field.itemTemplate(0), 0);
+
+        filterTemplate = field.filterTemplate();
+        equal(filterTemplate[0].tagName.toLowerCase(), "select");
+        equal(filterTemplate.children().length, 3);
+
+        insertTemplate = field.insertTemplate();
+        equal(insertTemplate[0].tagName.toLowerCase(), "select");
+        equal(insertTemplate.children().length, 3);
+
+        editTemplate = field.editTemplate(1);
+        equal(editTemplate[0].tagName.toLowerCase(), "select");
+        equal(editTemplate.find("option:selected").length, 1);
+        ok(editTemplate.children().eq(1).is(":selected"));
+
+        strictEqual(field.filterValue(), 0);
+        strictEqual(field.insertValue(), 0);
+        strictEqual(field.editValue(), 1);
+    });
+
+    test("string value type", function() {
+        var field = new jsGrid.SelectField({
+            name: "testField",
+            items: [
+                { text: "test1", value: "1" },
+                { text: "test2", value: "2" },
+                { text: "test3", value: "3" }
+            ],
+            textField: "text",
+            valueField: "value",
+            valueType: "string",
+            selectedIndex: 1
+        });
+
+        field.filterTemplate();
+        strictEqual(field.filterValue(), "2");
+
+        field.editTemplate("2");
+        strictEqual(field.editValue(), "2");
+
+        field.insertTemplate();
+        strictEqual(field.insertValue(), "2");
+    });
+
+    test("value type auto-defined", function() {
+        var field = new jsGrid.SelectField({
+            name: "testField",
+            items: [
+                { text: "test1", value: "1" },
+                { text: "test2", value: "2" },
+                { text: "test3", value: "3" }
+            ],
+            textField: "text",
+            valueField: "value",
+            selectedIndex: 1
+        });
+
+        strictEqual(field.sorter, "string", "sorter set according to value type");
+
+        field.filterTemplate();
+        strictEqual(field.filterValue(), "2");
+
+        field.editTemplate("2");
+        strictEqual(field.editValue(), "2");
+
+        field.insertTemplate();
+        strictEqual(field.insertValue(), "2");
+    });
+
+    test("value type defaulted to string", function() {
+        var field = new jsGrid.SelectField({
+            name: "testField",
+            items: [
+                { text: "test1" },
+                { text: "test2", value: "2" }
+            ],
+            textField: "text",
+            valueField: "value"
+        });
+
+        strictEqual(field.sorter, "string", "sorter set to string if first item has no value field");
+    });
+
+    test("object items", function() {
+        var field = new jsGrid.SelectField({
+            name: "testField",
+            items: [
+                { text: "test1", value: 1 },
+                { text: "test2", value: 2 },
+                { text: "test3", value: 3 }
+            ]
+        });
+
+        strictEqual(field.itemTemplate(1), field.items[1]);
+
+        field.textField = "text";
+        strictEqual(field.itemTemplate(1), "test2");
+
+        field.textField = "";
+        field.valueField = "value";
+        strictEqual(field.itemTemplate(1), field.items[0]);
+        ok(field.editTemplate(2));
+        strictEqual(field.editValue(), 2);
+
+        field.textField = "text";
+        strictEqual(field.itemTemplate(1), "test1");
+    });
+
+
+    module("jsGrid.field.control");
+
+    test("basic", function() {
+        var field,
+            itemTemplate,
+            headerTemplate,
+            filterTemplate,
+            insertTemplate,
+            editTemplate;
+
+        field = new jsGrid.ControlField();
+        field._grid = {
+            filtering: true,
+            inserting: true,
+            option: $.noop
+        };
+
+        itemTemplate = field.itemTemplate("any_value");
+        equal(itemTemplate.filter("." + field.editButtonClass).length, 1);
+        equal(itemTemplate.filter("." + field.deleteButtonClass).length, 1);
+
+        headerTemplate = field.headerTemplate();
+        equal(headerTemplate.filter("." + field.insertModeButtonClass).length, 1);
+
+        var $modeSwitchButton = headerTemplate.filter("." + field.modeButtonClass);
+        $modeSwitchButton.trigger("click");
+
+        equal(headerTemplate.filter("." + field.searchModeButtonClass).length, 1);
+
+        filterTemplate = field.filterTemplate();
+        equal(filterTemplate.filter("." + field.searchButtonClass).length, 1);
+        equal(filterTemplate.filter("." + field.clearFilterButtonClass).length, 1);
+
+        insertTemplate = field.insertTemplate();
+        equal(insertTemplate.filter("." + field.insertButtonClass).length, 1);
+
+        editTemplate = field.editTemplate("any_value");
+        equal(editTemplate.filter("." + field.updateButtonClass).length, 1);
+        equal(editTemplate.filter("." + field.cancelEditButtonClass).length, 1);
+
+        strictEqual(field.filterValue(), "");
+        strictEqual(field.insertValue(), "");
+        strictEqual(field.editValue(), "");
+    });
+
+    test("switchMode button should consider filtering=false", function() {
+        var optionArgs = {};
+
+        var field = new jsGrid.ControlField();
+        field._grid = {
+            filtering: false,
+            inserting: true,
+            option: function(name, value) {
+                optionArgs = {
+                    name: name,
+                    value: value
+                };
+            }
+        };
+
+        var headerTemplate = field.headerTemplate();
+        equal(headerTemplate.filter("." + field.insertModeButtonClass).length, 1, "inserting switch button rendered");
+
+        var $modeSwitchButton = headerTemplate.filter("." + field.modeButtonClass);
+
+        $modeSwitchButton.trigger("click");
+        ok($modeSwitchButton.hasClass(field.modeOnButtonClass), "on class is attached");
+        equal(headerTemplate.filter("." + field.insertModeButtonClass).length, 1, "insert button rendered");
+        equal(headerTemplate.filter("." + field.searchModeButtonClass).length, 0, "search button not rendered");
+        deepEqual(optionArgs, { name: "inserting", value: true }, "turn on grid inserting mode");
+
+        $modeSwitchButton.trigger("click");
+        ok(!$modeSwitchButton.hasClass(field.modeOnButtonClass), "on class is detached");
+        deepEqual(optionArgs, { name: "inserting", value: false }, "turn off grid inserting mode");
+    });
+
+    test("switchMode button should consider inserting=false", function() {
+        var optionArgs = {};
+
+        var field = new jsGrid.ControlField();
+        field._grid = {
+            filtering: true,
+            inserting: false,
+            option: function(name, value) {
+                optionArgs = {
+                    name: name,
+                    value: value
+                };
+            }
+        };
+
+        var headerTemplate = field.headerTemplate();
+        equal(headerTemplate.filter("." + field.searchModeButtonClass).length, 1, "filtering switch button rendered");
+
+        var $modeSwitchButton = headerTemplate.filter("." + field.modeButtonClass);
+
+        $modeSwitchButton.trigger("click");
+        ok(!$modeSwitchButton.hasClass(field.modeOnButtonClass), "on class is detached");
+        equal(headerTemplate.filter("." + field.searchModeButtonClass).length, 1, "search button rendered");
+        equal(headerTemplate.filter("." + field.insertModeButtonClass).length, 0, "insert button not rendered");
+        deepEqual(optionArgs, { name: "filtering", value: false }, "turn off grid filtering mode");
+
+        $modeSwitchButton.trigger("click");
+        ok($modeSwitchButton.hasClass(field.modeOnButtonClass), "on class is attached");
+        deepEqual(optionArgs, { name: "filtering", value: true }, "turn on grid filtering mode");
+    });
+
+    test("switchMode is not rendered if inserting=false and filtering=false", function() {
+        var optionArgs = {};
+
+        var field = new jsGrid.ControlField();
+        field._grid = {
+            filtering: false,
+            inserting: false
+        };
+
+        var headerTemplate = field.headerTemplate();
+        strictEqual(headerTemplate, "", "empty header");
+    });
+
+});

+ 51 - 0
src/api/static/js/jsgrid/tests/jsgrid.sort-strategies.tests.js

@@ -0,0 +1,51 @@
+$(function() {
+
+    var sortStrategies = jsGrid.sortStrategies;
+
+
+    module("sortStrategies");
+
+    test("string sorting", function() {
+        var data = ["c", "a", "d", "b"];
+
+        data.sort(sortStrategies["string"]);
+
+        deepEqual(data, ["a", "b", "c", "d"]);
+    });
+
+    test("string sorting should be robust", function() {
+        var data = ["a", 1, true, "b"];
+
+        data.sort(sortStrategies["string"]);
+
+        deepEqual(data, [1, "a", "b", true]);
+    });
+
+    test("number sorting", function() {
+        var data = [5, 3.2, 1e2, 4];
+
+        data.sort(sortStrategies["number"]);
+
+        deepEqual(data, [3.2, 4, 5, 100]);
+    });
+
+    test("date sorting", function() {
+        var date1 = new Date(2010, 0, 1),
+            date2 = new Date(2011, 0, 1),
+            date3 = new Date(2012, 0, 1);
+
+        var data = [date2, date3, date1];
+
+        data.sort(sortStrategies["date"]);
+
+        deepEqual(data, [date1, date2, date3]);
+    });
+
+    test("numberAsString sorting", function() {
+        var data = [".1", "2.1", "4e5", "2"];
+
+        data.sort(sortStrategies["numberAsString"]);
+
+        deepEqual(data, [".1", "2", "2.1", "4e5"]);
+    });
+});

Разница между файлами не показана из-за своего большого размера
+ 2821 - 0
src/api/static/js/jsgrid/tests/jsgrid.tests.js


+ 279 - 0
src/api/static/js/jsgrid/tests/jsgrid.validation.tests.js

@@ -0,0 +1,279 @@
+$(function() {
+
+    var validators = jsGrid.validators;
+
+
+    module("validation.validate", {
+        setup: function() {
+            this.validation = new jsGrid.Validation();
+        }
+    });
+
+    test("as function", function() {
+        var validateFunction = function(value) {
+            return value === "test";
+        };
+
+        deepEqual(this.validation.validate({
+            value: "not_test",
+            rules: validateFunction
+        }), [undefined]);
+
+        deepEqual(this.validation.validate({
+            value: "test",
+            rules: validateFunction
+        }), []);
+    });
+
+    test("as rule config", function() {
+        var validateRule = {
+            validator: function(value) {
+                return value === "test";
+            },
+            message: "Error"
+        };
+
+        deepEqual(this.validation.validate({
+            value: "not_test",
+            rules: validateRule
+        }), ["Error"]);
+
+        deepEqual(this.validation.validate({
+            value: "test",
+            rules: validateRule
+        }), []);
+    });
+
+    test("as rule config with param", function() {
+        var validateRule = {
+            validator: function(value, item, param) {
+                return value === param;
+            },
+            param: "test",
+            message: "Error"
+        };
+
+        deepEqual(this.validation.validate({
+            value: "not_test",
+            rules: validateRule
+        }), ["Error"]);
+
+        deepEqual(this.validation.validate({
+            value: "test",
+            rules: validateRule
+        }), []);
+    });
+
+    test("as array of rules", function() {
+        var validateRules = [{
+            message: "Error",
+            validator: function(value) {
+                return value !== "";
+            }
+        }, {
+            validator: function(value) {
+                return value === "test";
+            }
+        }];
+
+        deepEqual(this.validation.validate({
+            value: "",
+            rules: validateRules
+        }), ["Error", undefined]);
+
+        deepEqual(this.validation.validate({
+            value: "test",
+            rules: validateRules
+        }), []);
+    });
+
+    test("as string", function() {
+        validators.test_validator = function(value) {
+            return value === "test";
+        };
+
+        deepEqual(this.validation.validate({
+            value: "not_test",
+            rules: "test_validator"
+        }), [undefined]);
+
+        deepEqual(this.validation.validate({
+            value: "test",
+            rules: "test_validator"
+        }), []);
+
+        delete validators.test_validator;
+    });
+
+    test("as rule config with validator as string", function() {
+        validators.test_validator = function(value) {
+            return value === "test";
+        };
+
+        var validateRule = {
+            validator: "test_validator",
+            message: "Error"
+        };
+
+        deepEqual(this.validation.validate({
+            value: "not_test",
+            rules: validateRule
+        }), ["Error"]);
+
+        deepEqual(this.validation.validate({
+            value: "test",
+            rules: validateRule
+        }), []);
+
+        delete validators.test_validator;
+    });
+
+    test("as array of mixed rules", function() {
+        validators.test_validator = function(value) {
+            return value === "test";
+        };
+
+        var validationRules = [
+            "test_validator",
+            function(value) {
+                return value !== "";
+            }, {
+                validator: function(value) {
+                    return value === "test";
+                },
+                message: "Error"
+            }
+        ];
+
+        deepEqual(this.validation.validate({
+            value: "",
+            rules: validationRules
+        }), [undefined, undefined, "Error"]);
+
+        deepEqual(this.validation.validate({
+            value: "not_test",
+            rules: validationRules
+        }), [undefined, "Error"]);
+
+        deepEqual(this.validation.validate({
+            value: "test",
+            rules: validationRules
+        }), []);
+
+        delete validators.test_validator;
+    });
+
+    test("as string validator with default error message", function() {
+        validators.test_validator = {
+            message: function(value) {
+                return "Error: " + value;
+            },
+            validator: function(value) {
+                return value === "test";
+            }
+        };
+
+        var validateRule = {
+            validator: "test_validator"
+        };
+
+        deepEqual(this.validation.validate({
+            value: "not_test",
+            rules: validateRule
+        }), ["Error: not_test"]);
+
+        deepEqual(this.validation.validate({
+            value: "test",
+            rules: validateRule
+        }), []);
+
+        delete validators.test_validator;
+    });
+
+    test("throws exception for unknown validator", function() {
+        var validateRule = {
+            validator: "unknown_validator"
+        };
+
+        var validation = this.validation;
+
+        throws(function() {
+            validation.validate({
+                value: "test",
+                rules: validateRule
+            });
+        }, /unknown validator "unknown_validator"/, "exception for unknown validator");
+    });
+
+
+    module("validators", {
+        setup: function() {
+            var validation = new jsGrid.Validation();
+
+            this.testValidator = function(validator, value, param) {
+                var result = validation.validate({
+                    value: value,
+                    rules: { validator: validator, param: param }
+                });
+
+                return !result.length;
+            }
+        }
+    });
+
+    test("required", function() {
+        equal(this.testValidator("required", ""), false);
+        equal(this.testValidator("required", undefined), false);
+        equal(this.testValidator("required", null), false);
+        equal(this.testValidator("required", 0), true);
+        equal(this.testValidator("required", "test"), true);
+    });
+
+    test("rangeLength", function() {
+        equal(this.testValidator("rangeLength", "123456", [0, 5]), false);
+        equal(this.testValidator("rangeLength", "", [1, 5]), false);
+        equal(this.testValidator("rangeLength", "123", [0, 5]), true);
+        equal(this.testValidator("rangeLength", "", [0, 5]), true);
+        equal(this.testValidator("rangeLength", "12345", [0, 5]), true);
+    });
+
+    test("minLength", function() {
+        equal(this.testValidator("minLength", "123", 5), false);
+        equal(this.testValidator("minLength", "12345", 5), true);
+        equal(this.testValidator("minLength", "123456", 5), true);
+    });
+
+    test("maxLength", function() {
+        equal(this.testValidator("maxLength", "123456", 5), false);
+        equal(this.testValidator("maxLength", "12345", 5), true);
+        equal(this.testValidator("maxLength", "123", 5), true);
+    });
+
+    test("pattern", function() {
+        equal(this.testValidator("pattern", "_13_", "1?3"), false);
+        equal(this.testValidator("pattern", "13", "1?3"), true);
+        equal(this.testValidator("pattern", "3", "1?3"), true);
+        equal(this.testValidator("pattern", "_13_", /1?3/), true);
+    });
+
+    test("range", function() {
+        equal(this.testValidator("range", 6, [0, 5]), false);
+        equal(this.testValidator("range", 0, [1, 5]), false);
+        equal(this.testValidator("range", 3, [0, 5]), true);
+        equal(this.testValidator("range", 0, [0, 5]), true);
+        equal(this.testValidator("range", 5, [0, 5]), true);
+    });
+
+    test("min", function() {
+        equal(this.testValidator("min", 3, 5), false);
+        equal(this.testValidator("min", 5, 5), true);
+        equal(this.testValidator("min", 6, 5), true);
+    });
+
+    test("max", function() {
+        equal(this.testValidator("max", 6, 5), false);
+        equal(this.testValidator("max", 5, 5), true);
+        equal(this.testValidator("max", 3, 5), true);
+    });
+
+});

+ 15 - 0
src/api/templates/dashboard.html

@@ -0,0 +1,15 @@
+<link type="text/css" rel="stylesheet" href="{{ context }}/static/js/jsgrid/jsgrid.min.css" >
+<link type="text/css" rel="stylesheet" href="{{ context }}/static/js/jsgrid/jsgrid-theme.min.css" >
+<link href="{{ context }}/static/css/default.css" rel="stylesheet">
+<script src="{{ context }}/static/js/jquery/jquery.min.js"></script>
+
+<script src="{{ context }}/static/js/jsgrid/jsgrid.js"></script>
+
+<title></title>
+<body>
+<div class="caption">
+<div class="left" align="center">Process Monitoring
+	<div class="small">Latest Process Logs</div>
+</div>
+</div>
+</body>