├── .bowerrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.yaml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── SECURITY.md ├── bower.json ├── demos ├── cell-view │ ├── company-name.jade │ ├── demo.config.json │ ├── index.css │ ├── index.jade │ ├── index.js │ ├── js-data-resource.js │ ├── key-column-header.jade │ ├── map-view.jade │ ├── map-view.js │ └── pager-view-plugin.js ├── factory │ ├── company-name.jade │ ├── demo.config.json │ ├── index.jade │ ├── index.js │ ├── js-data-resource.js │ ├── key-column-header.jade │ └── pager-view-plugin.js ├── fixed-header │ ├── demo.config.json │ ├── index.jade │ ├── index.js │ └── index.less ├── full │ ├── demo.config.json │ ├── example.css │ ├── index.jade │ ├── index.js │ ├── northwind-schema.js │ └── specs │ │ └── index.js ├── js-data │ ├── demo.config.json │ ├── index.jade │ ├── index.js │ └── js-data-resource.js ├── odata │ ├── demo.config.json │ ├── index.jade │ └── index.js ├── radio-selection │ ├── company-name.jade │ ├── demo.config.json │ ├── index.jade │ ├── index.js │ ├── js-data-resource.js │ └── key-column-header.jade ├── vnext │ ├── columns │ │ └── README.md │ ├── data-source │ │ ├── README.md │ │ ├── custom │ │ │ ├── demo.config.json │ │ │ ├── index.jade │ │ │ ├── index.js │ │ │ ├── index.less │ │ │ └── js-data-resource.js │ │ ├── jsdata │ │ │ ├── demo.config.json │ │ │ ├── index.jade │ │ │ ├── index.js │ │ │ ├── index.less │ │ │ └── js-data-resource.js │ │ ├── memory │ │ │ ├── demo.config.json │ │ │ ├── index.jade │ │ │ ├── index.js │ │ │ ├── index.less │ │ │ └── people.json │ │ └── odata │ │ │ ├── demo.config.json │ │ │ ├── index.jade │ │ │ ├── index.js │ │ │ └── index.less │ ├── events │ │ ├── README.md │ │ ├── demo.config.json │ │ ├── index.jade │ │ ├── index.js │ │ └── index.less │ ├── pagination │ │ ├── demo.config.json │ │ ├── index.jade │ │ ├── index.js │ │ ├── index.less │ │ └── pager-view-plugin.js │ ├── rows │ │ ├── README.md │ │ ├── demo.config.json │ │ ├── index.jade │ │ ├── index.js │ │ └── index.less │ ├── scrolling │ │ ├── README.md │ │ ├── columns.js │ │ ├── emails.jade │ │ ├── fixed-header │ │ │ ├── demo.config.json │ │ │ ├── index.jade │ │ │ ├── index.js │ │ │ └── index.less │ │ ├── people.json │ │ ├── rows.js │ │ ├── static-header │ │ │ ├── demo.config.json │ │ │ ├── index.jade │ │ │ ├── index.js │ │ │ └── index.less │ │ ├── sticky-header-on-element-flex │ │ │ ├── demo.config.json │ │ │ ├── index.jade │ │ │ ├── index.js │ │ │ └── index.less │ │ ├── sticky-header-on-element │ │ │ ├── demo.config.json │ │ │ ├── index.jade │ │ │ ├── index.js │ │ │ └── index.less │ │ └── sticky-header-on-window │ │ │ ├── demo.config.json │ │ │ ├── index.jade │ │ │ ├── index.js │ │ │ └── index.less │ ├── selection │ │ ├── multiple │ │ │ ├── demo.config.json │ │ │ ├── index.jade │ │ │ ├── index.js │ │ │ └── index.less │ │ └── single │ │ │ ├── demo.config.json │ │ │ ├── index.jade │ │ │ ├── index.js │ │ │ └── index.less │ └── sortable-header │ │ ├── README.md │ │ ├── demo.config.json │ │ ├── index.jade │ │ ├── index.js │ │ ├── index.less │ │ ├── sortable-header-alphabet.jade │ │ ├── sortable-header-number.jade │ │ └── sortable-header.jade └── webpack.config.js ├── gulpfile.js ├── js ├── .DS_Store ├── containerBase.js ├── elementContainer.js ├── factory │ ├── grid-factory.js │ ├── grid-view-plugin.js │ ├── projection-plugin.js │ ├── renderers-plugin.js │ └── utility.js ├── grid-view.js ├── index.js ├── layout │ ├── .DS_Store │ ├── index.js │ ├── measure.js │ ├── px.js │ ├── renderer │ │ ├── fixed-header.js │ │ ├── index.js │ │ └── virtualization.js │ ├── table.js │ └── template │ │ ├── editable.jade │ │ ├── row.editable.string.jade │ │ ├── row.tri-state-checked.jade │ │ ├── selectable.jade │ │ ├── table.dot │ │ ├── table.jade │ │ ├── table.mobile.jade │ │ └── table.while.jade ├── model │ ├── index.js │ └── response.js ├── popup-editor │ ├── index.jade │ └── index.js ├── projection │ ├── a11y.js │ ├── aggregate-row.js │ ├── base.js │ ├── column-group.js │ ├── column-hovertext.js │ ├── column-i18n.js │ ├── column-queryable.js │ ├── column-shifter.js │ ├── column-template.js │ ├── columns.js │ ├── editable-string.js │ ├── editable.js │ ├── index.js │ ├── jsdata.js │ ├── map.js │ ├── memory-filter.js │ ├── memory-page.js │ ├── memory-queryable.js │ ├── memory-sort.js │ ├── memory.js │ ├── mock.js │ ├── odata.js │ ├── page.js │ ├── property-template.js │ ├── row-checkbox.js │ ├── row-index.js │ ├── row-tri-state-checkbox.js │ ├── row.js │ └── sink.js ├── schema │ ├── list-view.js │ └── properties.js ├── vnext │ ├── data-source │ │ ├── base.js │ │ ├── index.js │ │ ├── js-data.js │ │ ├── memory.js │ │ └── odata.js │ ├── factory │ │ ├── data-source-plugin.js │ │ └── grid-view-plugin.js │ ├── grid-view.js │ ├── layout │ │ ├── column-group.jade │ │ ├── const.js │ │ ├── editable.jade │ │ ├── escape.js │ │ ├── flex-fixed.jade │ │ ├── flex-head-cell.jade │ │ ├── flex-header-footer.jade │ │ ├── flex-layout.less │ │ ├── flex-mixins.jade │ │ ├── flex-row.jade │ │ ├── flex-static.jade │ │ ├── flex-sticky.jade │ │ ├── header-footer-view.js │ │ ├── header-footer.jade │ │ ├── index.js │ │ ├── row.jade │ │ ├── table-fixed.jade │ │ ├── table-mixins.jade │ │ ├── table-static.jade │ │ ├── table-sticky.jade │ │ └── table-view.js │ └── projection │ │ ├── buffer.js │ │ ├── cells.js │ │ ├── column-group.js │ │ ├── columns.js │ │ ├── common.js │ │ ├── default-cell.jade │ │ ├── events.js │ │ ├── flex-columns.js │ │ ├── index.js │ │ ├── item-index.js │ │ ├── multiple-selection-resolver.js │ │ ├── query.js │ │ ├── range-selection.js │ │ ├── rows.js │ │ ├── selection-body.jade │ │ ├── selection-head.jade │ │ ├── selection-resolver.js │ │ ├── selection.js │ │ ├── single-selection-resolver.js │ │ ├── sortable-header.jade │ │ └── sortable-header.js └── windowContainer.js ├── jsdoc.json ├── less └── editable.less ├── package.json ├── spec ├── integrated │ ├── $speclist.js │ ├── conf │ │ ├── karma.conf.js │ │ ├── karma.debug.conf.js │ │ └── webpack.config.js │ ├── data │ │ ├── js-data-expected.json │ │ ├── js-data-source.js │ │ ├── people.json │ │ └── scrolling.json │ ├── driver │ │ ├── action.js │ │ ├── events.js │ │ ├── index.js │ │ ├── protocol.js │ │ └── utility.js │ ├── non-vnext-spec │ │ ├── aggregate-row-spec.js │ │ ├── columns-spec.js │ │ ├── data-source-spec.js │ │ ├── editable-spec.js │ │ ├── grid-view-api-spec.js │ │ ├── page-spec.js │ │ ├── scrollable-spec.js │ │ ├── selectable-spec.js │ │ └── unencapsulating-projection-spec.js │ ├── styles │ │ └── tri-state-checkbox.css │ ├── template │ │ ├── column-address.jade │ │ └── column-header.jade │ └── vnext-spec │ │ ├── column-spec.js │ │ ├── data-source-spec.js │ │ ├── flex-layout-spec.js │ │ ├── grid-view-api-spec.js │ │ ├── rows-spec.js │ │ ├── scrolling-spec.js │ │ └── selection-spec.js ├── unit │ ├── $speclist.js │ ├── .eslintrc.yaml │ ├── aggregate-row-spec.js │ ├── base-spec.js │ ├── column-i18n-spec.js │ ├── column-queryable-spec.js │ ├── column-shifter-spec.js │ ├── conf │ │ ├── karma.conf.js │ │ ├── karma.debug.conf.js │ │ └── webpack.config.js │ ├── editable-spec.js │ ├── jsdata-spec.js │ ├── map-spec.js │ ├── mock-spec.js │ ├── odata-spec.js │ ├── page-spec.js │ ├── property-template-spec.js │ ├── row-checkbox-spec.js │ ├── row-index-spec.js │ └── util.js └── util │ └── index.js ├── wdio.conf.js ├── webpack.alias.js ├── webpack.config.js └── yarn.lock /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "./lib/" 3 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # npm packages 2 | node_modules 3 | 4 | # code coverage 5 | coverage 6 | 7 | # output folder 8 | dist 9 | 10 | # generated code for example pages 11 | examples/requirejs/require.config.js 12 | examples/webpack/dist 13 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | "extends": "xo-space" 3 | "env": 4 | "mocha": true 5 | "amd": true 6 | "browser": true 7 | "rules": 8 | "comma-dangle": 9 | - 2 10 | - "always-multiline" 11 | "object-curly-spacing": 12 | - 2 13 | - "always" 14 | "linebreak-style": 0 15 | "no-warning-comments": 0 16 | "one-var": 17 | - 2 18 | - 19 | uninitialized: always 20 | initialized: never 21 | "no-unused-expressions": 22 | - 2 23 | - 24 | allowShortCircuit: true 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # npm packages 2 | node_modules 3 | 4 | # test results 5 | test-results 6 | errorShots 7 | 8 | # code coverage 9 | coverage 10 | 11 | # npm logs 12 | node-debug.log 13 | npm-debug.log 14 | 15 | # yarn logs 16 | yarn-error.log 17 | 18 | # output folder 19 | dist 20 | docs 21 | 22 | # the npm settings 23 | .npmrc 24 | 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | env: 3 | - CXX=g++-4.8 4 | addons: 5 | apt: 6 | sources: 7 | - ubuntu-toolchain-r-test 8 | packages: 9 | - g++-4.8 10 | sudo: true 11 | node_js: 12 | - v5 13 | before_install: 14 | - export CHROME_BIN=chromium-browser 15 | - export DISPLAY=:99.0 16 | - sh -e /etc/init.d/xvfb start 17 | script: 18 | - gulp test 19 | - "cat ./spec/*/coverage/report-lcov/lcov.info | ./node_modules/coveralls/bin/coveralls.js" 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Bing Ads 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "projection-grid", 3 | "description": "", 4 | "main": "", 5 | "authors": [ 6 | "Ahmed Kamel " 7 | ], 8 | "license": "MIT", 9 | "homepage": "https://github.com/BingAds/projection-grid", 10 | "moduleType": [ 11 | "amd" 12 | ], 13 | "private": true, 14 | "ignore": [ 15 | "**/.*", 16 | "node_modules", 17 | "bower_components", 18 | "lib/", 19 | "test", 20 | "tests" 21 | ], 22 | "dependencies": { 23 | "backbone": "backbone#1.2.3", 24 | "bootstrap": "bootstrap#v3.3.6", 25 | "jquery": "jquery#2.2.1", 26 | "requirejs": "requirejs#latest", 27 | "underscore": "underscore#1.8.3" 28 | }, 29 | "devDependencies": { 30 | "squire": "squire#v0.2.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /demos/cell-view/company-name.jade: -------------------------------------------------------------------------------- 1 | i 2 | span=model[property] 3 | -------------------------------------------------------------------------------- /demos/cell-view/demo.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "./index.jade" 3 | } 4 | -------------------------------------------------------------------------------- /demos/cell-view/index.css: -------------------------------------------------------------------------------- 1 | .container-map { 2 | position: fixed; 3 | right: 0; 4 | top: 0; 5 | width: 500px; 6 | height: 500px; 7 | } 8 | -------------------------------------------------------------------------------- /demos/cell-view/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= title 5 | body 6 | .container 7 | .row 8 | .grid-root 9 | .pager-root 10 | .container-map 11 | #myMap 12 | script(type="application/javascript", src="http://www.bing.com/api/maps/mapcontrol") 13 | script(type="application/javascript", src=bundle) 14 | -------------------------------------------------------------------------------- /demos/cell-view/index.js: -------------------------------------------------------------------------------- 1 | var pgrid = require('projection-grid'); 2 | var Customer = require('./js-data-resource').default; 3 | var keyHeaderTemplate = require('./key-column-header.jade'); 4 | var companyNameTemplate = require('./company-name.jade'); 5 | var MapView = require('./map-view').default; 6 | 7 | require('bootstrap-webpack'); 8 | require('style!css!./index.css'); 9 | 10 | var grid = pgrid.factory().create({ 11 | el: '.grid-root', 12 | dataSource: { 13 | type: 'js-data', 14 | resource: Customer, 15 | schema: { key: 'CustomerID' }, 16 | }, 17 | selectable: true, 18 | columns: [ 19 | { 20 | name: 'CustomerID', 21 | title: 'Customer ID', 22 | sortable: true, 23 | locked: true, 24 | headerTemplate: keyHeaderTemplate, 25 | }, 26 | { 27 | name: 'CompanyName', 28 | title: 'Company Name', 29 | sortable: true, 30 | template: companyNameTemplate, 31 | attributes: { 32 | class: 'company-name-cell', 33 | }, 34 | headerAttributes: { 35 | class: 'company-name-header', 36 | }, 37 | }, 38 | { 39 | name: 'City', 40 | title: 'City', 41 | sortable: true, 42 | View: MapView, 43 | }, 44 | { 45 | name: 'Contact', 46 | field: 'ContactName', 47 | value: item => `${item.ContactName} (${item.ContactTitle})`, 48 | sortable: true, 49 | }, 50 | ], 51 | }); 52 | 53 | grid.gridView.render({ fetch: true }); 54 | -------------------------------------------------------------------------------- /demos/cell-view/js-data-resource.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import JSData from 'js-data'; 3 | import DSHttpAdapter from 'js-data-http'; 4 | 5 | class ODataAdapter extends DSHttpAdapter { 6 | 7 | constructor(options) { 8 | super(_.extend(options, { 9 | queryTransform(definition, params) { 10 | const query = { $count: true }; 11 | 12 | _.has(params, 'offset') && (query.$skip = params.offset); 13 | _.has(params, 'limit') && (query.$top = params.limit); 14 | _.has(params, 'orderBy') && (query.$orderby = params.orderBy.map(function (item) { 15 | return item[0] + ' ' + item[1].toLowerCase(); 16 | }).join(',')); 17 | 18 | return query; 19 | }, 20 | })); 21 | } 22 | 23 | findAll(definition, params, options) { 24 | let totalCount = 0; 25 | 26 | options.afterFindAll = (resource, data) => { 27 | totalCount = data['@odata.count']; 28 | return data.value; 29 | }; 30 | options.afterInject = (resource, instances) => { 31 | Object.defineProperty(instances, 'totalCount', { value: totalCount }); 32 | }; 33 | 34 | return super.findAll(definition, params, options); 35 | } 36 | } 37 | 38 | JSData.DSUtils.Promise = require('bluebird'); 39 | 40 | const store = new JSData.DS(); 41 | store.registerAdapter('odata', new ODataAdapter({ 42 | basePath: 'http://services.odata.org/V4/Northwind/Northwind.svc/', 43 | }), { default: true }); 44 | 45 | export default store.defineResource({ 46 | name: 'Customers', 47 | idAttribute: 'CustomerID', 48 | }); 49 | -------------------------------------------------------------------------------- /demos/cell-view/key-column-header.jade: -------------------------------------------------------------------------------- 1 | i 2 | span=$text 3 | -------------------------------------------------------------------------------- /demos/cell-view/map-view.jade: -------------------------------------------------------------------------------- 1 | span=City 2 |   3 | button.btn.btn-default(type='button') Show 4 | -------------------------------------------------------------------------------- /demos/cell-view/map-view.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import Backbone from 'backbone'; 3 | import template from './map-view.jade'; 4 | 5 | var mapKey = 'Aitu1GXEVDLOMeRerogqYje1eqGGfi_anQ0uPmUkIqnCgwm72G2zdJj9a-sDS0Qe'; 6 | var map = null; 7 | 8 | function showCity(cityName) { 9 | $.getJSON('http://dev.virtualearth.net/REST/v1/Locations/?q=' + cityName + '&key=' + mapKey + '&jsonp=?', function (data) { 10 | var coordinates = data.resourceSets[0].resources[0].point.coordinates; 11 | var location = new window.Microsoft.Maps.Location(coordinates[0], coordinates[1]); 12 | map.setView({ 13 | center: location, 14 | zoom: 10, 15 | }); 16 | }); 17 | } 18 | 19 | function hideMap() { 20 | $('.container-map').hide(); 21 | } 22 | 23 | function showMap(cityName) { 24 | $('.container-map').show(); 25 | if (!map) { 26 | map = new window.Microsoft.Maps.Map(document.getElementById('myMap'), { 27 | credentials: mapKey, 28 | }); 29 | } 30 | showCity(cityName); 31 | 32 | function dismissOnClick(e) { 33 | if (!$(e.target).closest('#myMap').length) { 34 | hideMap(); 35 | $('body').off('click', dismissOnClick); 36 | } 37 | } 38 | $('body').on('click', dismissOnClick); 39 | } 40 | 41 | hideMap(); 42 | 43 | export default class MapView extends Backbone.View { 44 | events() { 45 | return { 46 | 'click button': e => { 47 | showMap(this.model.City); 48 | e.stopImmediatePropagation(); 49 | }, 50 | }; 51 | } 52 | 53 | initialize() { 54 | } 55 | 56 | render() { 57 | this.$el.html(template(this.model)); 58 | return this; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /demos/cell-view/pager-view-plugin.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import { PaginationView } from 'pagination-control'; 3 | 4 | export default definePlugin => definePlugin('pagerView', [ 5 | 'config', 6 | 'projection', 7 | 'gridView', 8 | ], function (config, projection, gridView) { 9 | const pagerView = new PaginationView(_.defaults({ 10 | pageSize: config.pageable.pageSize, 11 | availablePageSizes: config.pageable.pageSizes, 12 | }, config.pagerView)); 13 | 14 | gridView.on('change:data', function (model) { 15 | pagerView.itemCount = model.get('count'); 16 | }); 17 | 18 | pagerView.on('change:page-size', function (pageSize) { 19 | projection.set('page.size', pageSize); 20 | }); 21 | 22 | pagerView.on('change:page-number', function (pageNumber) { 23 | projection.set('page.number', pageNumber); 24 | }); 25 | 26 | return pagerView; 27 | }); 28 | -------------------------------------------------------------------------------- /demos/factory/company-name.jade: -------------------------------------------------------------------------------- 1 | i 2 | span=model[property] 3 | -------------------------------------------------------------------------------- /demos/factory/demo.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "./index.jade" 3 | } 4 | -------------------------------------------------------------------------------- /demos/factory/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= title 5 | body 6 | .container 7 | .row 8 | .grid-root 9 | .pager-root 10 | script(type="application/javascript", src=bundle) 11 | -------------------------------------------------------------------------------- /demos/factory/index.js: -------------------------------------------------------------------------------- 1 | var pgrid = require('projection-grid'); 2 | var Customer = require('./js-data-resource'); 3 | var keyHeaderTemplate = require('./key-column-header.jade'); 4 | var companyNameTemplate = require('./company-name.jade'); 5 | var pagerViewPlugin = require('./pager-view-plugin').default; 6 | require('bootstrap-webpack'); 7 | 8 | var grid = pgrid.factory().use(pagerViewPlugin).create({ 9 | el: '.grid-root', 10 | dataSource: { 11 | type: 'js-data', 12 | resource: Customer, 13 | schema: { key: 'CustomerID' }, 14 | }, 15 | aggregate: { 16 | top(/* data */) { 17 | return [ 18 | { CustomerID: 'Total Top' }, 19 | ]; 20 | }, 21 | bottom(/* data */) { 22 | return [ 23 | { CustomerID: 'Total Bottom' }, 24 | ]; 25 | }, 26 | }, 27 | scrollable: { 28 | virtual: true, 29 | }, 30 | columnShifter: { 31 | totalColumns: 3, 32 | }, 33 | selectable: true, 34 | pageable: { 35 | pageSize: 10, 36 | pageSizes: [5, 10, 15, 20], 37 | }, 38 | editable: { 39 | tooltipText: 'Edit Me!', 40 | iconClasses: ['glyphicon', 'glyphicon-wrench'], 41 | }, 42 | columns: [ 43 | { 44 | name: 'CustomerID', 45 | title: 'Customer ID', 46 | hoverText: 'This is hover text', 47 | sortable: true, 48 | locked: true, 49 | headerTemplate: keyHeaderTemplate, 50 | }, 51 | { 52 | name: 'CompanyName', 53 | title: 'Company Name', 54 | sortable: true, 55 | editable: true, 56 | template: companyNameTemplate, 57 | attributes: { 58 | class: 'company-name-cell', 59 | }, 60 | headerAttributes: { 61 | class: 'company-name-header', 62 | }, 63 | }, 64 | { 65 | name: 'City', 66 | title: 'City', 67 | sortable: true, 68 | editable: true, 69 | headerBuilder: (column => 'Great ' + column.property), 70 | }, 71 | { 72 | name: 'Contact', 73 | field: 'ContactName', 74 | value: item => `${item.ContactName} (${item.ContactTitle})`, 75 | sortable: true, 76 | }, 77 | ], 78 | pagerView: { 79 | el: '.pager-root', 80 | availablePageSizes: [5, 10, 15, 20], 81 | }, 82 | }); 83 | 84 | grid.gridView.on('update:beginning', function () { 85 | console.log('begin update'); 86 | }); 87 | 88 | grid.gridView.on('update:finished', function (error) { 89 | if (error) { 90 | console.log('fail update'); 91 | } else { 92 | console.log('success update'); 93 | } 94 | }); 95 | 96 | grid.gridView.on('data:edit', model => { 97 | console.log(`[Edit] ${JSON.stringify(model)}`); 98 | }); 99 | 100 | grid.gridView.render({ fetch: true }); 101 | grid.pagerView.render(); 102 | -------------------------------------------------------------------------------- /demos/factory/js-data-resource.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var JSData = require('js-data'); 3 | var DSHttpAdapter = require('js-data-http'); 4 | 5 | class ODataAdapter extends DSHttpAdapter { 6 | 7 | constructor(options) { 8 | super(_.extend(options, { 9 | queryTransform(definition, params) { 10 | var query = { $count: true }; 11 | 12 | _.has(params, 'offset') && (query.$skip = params.offset); 13 | _.has(params, 'limit') && (query.$top = params.limit); 14 | _.has(params, 'orderBy') && (query.$orderby = params.orderBy.map(function (item) { 15 | return item[0] + ' ' + item[1].toLowerCase(); 16 | }).join(',')); 17 | 18 | return query; 19 | }, 20 | })); 21 | } 22 | 23 | findAll(definition, params, options) { 24 | let totalCount = 0; 25 | 26 | options.afterFindAll = (resource, data) => { 27 | totalCount = data['@odata.count']; 28 | return data.value; 29 | }; 30 | options.afterInject = (resource, instances) => { 31 | Object.defineProperty(instances, 'totalCount', { value: totalCount }); 32 | }; 33 | 34 | return super.findAll(definition, params, options); 35 | } 36 | } 37 | 38 | JSData.DSUtils.Promise = require('bluebird'); 39 | 40 | var store = new JSData.DS(); 41 | store.registerAdapter('odata', new ODataAdapter({ 42 | basePath: 'http://services.odata.org/V4/Northwind/Northwind.svc/', 43 | }), { default: true }); 44 | 45 | module.exports = store.defineResource({ 46 | name: 'Customers', 47 | idAttribute: 'CustomerID', 48 | }); 49 | -------------------------------------------------------------------------------- /demos/factory/key-column-header.jade: -------------------------------------------------------------------------------- 1 | i 2 | span=$text 3 | -------------------------------------------------------------------------------- /demos/factory/pager-view-plugin.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import { PaginationView } from 'pagination-control'; 3 | 4 | export default definePlugin => definePlugin('pagerView', [ 5 | 'config', 6 | 'projection', 7 | 'gridView', 8 | ], function (config, projection, gridView) { 9 | const pagerView = new PaginationView(_.defaults({ 10 | pageSize: config.pageable.pageSize, 11 | availablePageSizes: config.pageable.pageSizes, 12 | }, config.pagerView)); 13 | 14 | gridView.on('change:data', function (model) { 15 | pagerView.itemCount = model.get('count'); 16 | }); 17 | 18 | pagerView.on('change:page-size', function (pageSize) { 19 | projection.set('page.size', pageSize); 20 | }); 21 | 22 | pagerView.on('change:page-number', function (pageNumber) { 23 | projection.set('page.number', pageNumber); 24 | }); 25 | 26 | return pagerView; 27 | }); 28 | -------------------------------------------------------------------------------- /demos/fixed-header/demo.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "./index.jade" 3 | } 4 | -------------------------------------------------------------------------------- /demos/fixed-header/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= title 5 | body 6 | .container 7 | .row 8 | .grid-root 9 | .pager-root 10 | script(type="application/javascript", src=bundle) 11 | -------------------------------------------------------------------------------- /demos/fixed-header/index.js: -------------------------------------------------------------------------------- 1 | import pgrid from 'projection-grid'; 2 | import 'bootstrap-webpack'; 3 | import './index.less'; 4 | 5 | const grid = pgrid.factory().create({ 6 | el: '.grid-root', 7 | dataSource: { 8 | type: 'odata', 9 | url: 'http://services.odata.org/V4/Northwind/Northwind.svc/Customers', 10 | }, 11 | columns: [ 12 | { name: 'CustomerID', sortable: true }, 13 | { name: 'CompanyName', sortable: true }, 14 | { name: 'City', sortable: true }, 15 | { name: 'ContactName', sortable: true }, 16 | ], 17 | scrollable: { 18 | fixedHeader: true, 19 | }, 20 | layoutOptions: { 21 | class: 'table-bordered', 22 | }, 23 | }); 24 | 25 | grid.gridView.on('update:beginning', function () { 26 | console.log('begin update'); 27 | }); 28 | 29 | grid.gridView.on('update:finished', function () { 30 | console.log('end update'); 31 | }); 32 | 33 | grid.gridView.render({ fetch: true }); 34 | -------------------------------------------------------------------------------- /demos/fixed-header/index.less: -------------------------------------------------------------------------------- 1 | .col-CustomerID { 2 | width: 100px; 3 | min-width: 100px; 4 | } 5 | 6 | .col-CompanyName { 7 | width: 150px; 8 | min-width: 150px; 9 | } 10 | 11 | .col-City { 12 | width: 100px; 13 | min-width: 100px; 14 | } 15 | 16 | .col-ContactName { 17 | width: 200px; 18 | min-width: 200px; 19 | } 20 | 21 | -------------------------------------------------------------------------------- /demos/full/demo.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "./index.jade" 3 | } 4 | -------------------------------------------------------------------------------- /demos/full/example.css: -------------------------------------------------------------------------------- 1 | table > thead > tr { 2 | background-color: #FFF; 3 | } 4 | 5 | .table { 6 | /*table-layout: fixed;*/ 7 | width: 1200px; 8 | overflow: hidden; 9 | } 10 | 11 | .table thead td { 12 | /*display: flex;*/ 13 | } 14 | 15 | .table td, .table th { 16 | word-break: break-word; 17 | /*outline: solid gray 1px;*/ 18 | } 19 | 20 | .table__row--aggregate td { 21 | background-color: #f5f5f5; 22 | } 23 | 24 | .table__row--body td:hover { 25 | background-color: #ddd; 26 | } 27 | 28 | .column-tri-state-checkbox { 29 | border: 1px solid rgba(155,155,155,0.57); 30 | border-radius: 1px; 31 | height: 21px; 32 | margin: 0 1px; 33 | outline: 0; 34 | vertical-align: text-bottom; 35 | width: 21px; 36 | } 37 | 38 | .column-tri-state-checkbox > span.glyphicon { 39 | left: 3px; 40 | } 41 | -------------------------------------------------------------------------------- /demos/full/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= title 5 | body 6 | .container 7 | .row 8 | .panel.panel-info 9 | .panel-heading Commands you can run in the console 10 | .panel-body 11 | pre. 12 | -- skip and take only work when pagination control is not connected [for now] 13 | // window.grid.projection.set({ skip : 20, take : 100, filter : function(i){ return i.age < 5; } }); 14 | -- select is being overriden by the column shifter's options 15 | // window.grid.projection.set({ select : ['age', 'age', 'name', 'index', 'name', 'age', 'index', 'age'] }); 16 | -- call to column.take should work fine on this page 17 | // window.grid.projection.set({ 'column.take' : 4, 'column.skip' : 3 }); 18 | // --- only used to change column settings / dimensions; window.grid.set('columns', [{ property : 'name', title : 'My Name' }, { property : 'age', title : 'Cat Age' }]); 19 | .panel.panel-info 20 | .panel-heading Notes 21 | .panel-body 22 | ul 23 | li Column 'ContactName' is sortable 24 | li Shift Columns right and left 25 | li Use Pager to navigate OData Source 26 | .row 27 | #grid_toolbar_host_a 28 | #pagination_host_a 29 | #grid_host_a 30 | #pager 31 | 32 | .row 33 | .panel.panel-info 34 | .panel-heading Footer Space 35 | .panel-body 36 | ul 37 | li Used to make sure that grid virtualization renders correctly at the end of page 38 | script(type='text/javascript', src=bundle) 39 | -------------------------------------------------------------------------------- /demos/full/northwind-schema.js: -------------------------------------------------------------------------------- 1 | define({ 2 | title: 'Northwind Schema', 3 | type: 'object', 4 | properties: { 5 | CustomerID: { 6 | type: 'string', 7 | filterable: true, 8 | nullable: false, 9 | required: true, 10 | aggregateType: 'count', 11 | }, 12 | EmployeeID: { 13 | type: 'number', 14 | filterable: true, 15 | nullable: false, 16 | required: true, 17 | aggregateType: 'count', 18 | }, 19 | ShipName: { 20 | type: 'string', 21 | filterable: true, 22 | nullable: false, 23 | required: true, 24 | aggregateType: 'count', 25 | }, 26 | OrderID: { 27 | type: 'integer', 28 | }, 29 | Freight: { 30 | type: 'number', 31 | }, 32 | ShipCountry: { 33 | type: 'string', 34 | enum: [ 35 | 'France', 36 | 'Germany', 37 | 'Brazil', 38 | 'Belgium', 39 | 'Switzerland', 40 | 'Venezuela', 41 | 'Austria', 42 | 'Mexico', 43 | 'USA', 44 | 'Sweden', 45 | 'Finland', 46 | 'Spain', 47 | 'UK', 48 | 'Italy', 49 | 'Ireland', 50 | 'Portugal', 51 | 'Canada', 52 | 'Poland', 53 | 'Norway', 54 | 'Denmark', 55 | ], 56 | }, 57 | }, 58 | required: ['CustomerID'], 59 | }); 60 | -------------------------------------------------------------------------------- /demos/full/specs/index.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | 3 | /* global browser */ 4 | /* eslint no-unused-expressions: 0 */ 5 | describe('webdriver.io page', function () { 6 | beforeEach(function (cb) { 7 | browser.waitForExist('.table.grid', cb); 8 | }); 9 | 10 | it('should expand and collapse the group column', function () { 11 | var collpaseSpan = browser.element('.pop-collapse'); 12 | expect(collpaseSpan).to.be.exist; 13 | collpaseSpan.click(); 14 | var expandBtn = browser.element('.pop-expand'); 15 | expect(expandBtn).to.be.exist; 16 | expandBtn.click(); 17 | expect(browser.isExisting('.pop-collapse')).to.be.true; 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /demos/js-data/demo.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "./index.jade" 3 | } 4 | -------------------------------------------------------------------------------- /demos/js-data/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= title 5 | body 6 | .container 7 | .row 8 | .grid-root 9 | .pager-root 10 | script(type="application/javascript", src=bundle) 11 | -------------------------------------------------------------------------------- /demos/js-data/index.js: -------------------------------------------------------------------------------- 1 | var pgrid = require('projection-grid'); 2 | var Customer = require('./js-data-resource'); 3 | 4 | var jsdata = new pgrid.projections.JSData({ 5 | 'jsdata.entity': Customer, 6 | }); 7 | var projection = jsdata.pipe( 8 | new pgrid.projections.ColumnI18n() 9 | ).pipe(new pgrid.projections.ColumnQueryable({ 10 | 'column.take': 10, 11 | })); 12 | 13 | var grid = new pgrid.GridView({ 14 | el: '.grid-root', 15 | projection: projection, 16 | Layout: pgrid.layout.TableLayout.partial({ 17 | template: pgrid.layout.templates.table, 18 | renderers: [ 19 | pgrid.layout.renderers.Virtualization, 20 | ], 21 | columns: { 22 | CustomerID: { sortable: true }, 23 | CompanyName: { sortable: true }, 24 | City: { sortable: true }, 25 | ContactName: { sortable: true }, 26 | }, 27 | }), 28 | }); 29 | 30 | jsdata.on('update:beginning', function () { 31 | console.log('begin update'); 32 | }); 33 | 34 | jsdata.on('update:finished', function () { 35 | console.log('end update'); 36 | }); 37 | 38 | grid.render({ fetch: true }); 39 | -------------------------------------------------------------------------------- /demos/js-data/js-data-resource.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var JSData = require('js-data'); 3 | var DSHttpAdapter = require('js-data-http'); 4 | 5 | JSData.DSUtils.Promise = require('bluebird'); 6 | 7 | var store = new JSData.DS(); 8 | store.registerAdapter('http', new DSHttpAdapter({ 9 | basePath: 'http://services.odata.org/V4/Northwind/Northwind.svc/', 10 | deserialize: function (definition, response) { 11 | return response.data.value; 12 | }, 13 | queryTransform: function (definition, params) { 14 | var query = {}; 15 | 16 | _.has(params, 'offset') && (query.$skip = params.offset); 17 | _.has(params, 'limit') && (query.$top = params.limit); 18 | _.has(params, 'orderBy') && (query.$orderby = params.orderBy.map(function (item) { 19 | return item[0] + ' ' + item[1].toLowerCase(); 20 | }).join(',')); 21 | 22 | return query; 23 | }, 24 | }), { default: true }); 25 | 26 | module.exports = store.defineResource({ 27 | name: 'Customers', 28 | idAttribute: 'CustomerID', 29 | }); 30 | -------------------------------------------------------------------------------- /demos/odata/demo.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "./index.jade" 3 | } 4 | -------------------------------------------------------------------------------- /demos/odata/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= title 5 | body 6 | .container 7 | .row 8 | .grid-root 9 | .pager-root 10 | script(type="application/javascript", src=bundle) 11 | -------------------------------------------------------------------------------- /demos/odata/index.js: -------------------------------------------------------------------------------- 1 | import pgrid from 'projection-grid'; 2 | 3 | const grid = pgrid.factory().create({ 4 | el: '.grid-root', 5 | dataSource: { 6 | type: 'odata', 7 | url: 'http://services.odata.org/V4/Northwind/Northwind.svc/Customers', 8 | }, 9 | columns: [ 10 | { name: 'CustomerID', sortable: true }, 11 | { name: 'CompanyName', sortable: true }, 12 | { name: 'City', sortable: true }, 13 | { name: 'ContactName', sortable: true }, 14 | ], 15 | }); 16 | 17 | grid.gridView.on('update:beginning', function () { 18 | console.log('begin update'); 19 | }); 20 | 21 | grid.gridView.on('update:finished', function (error) { 22 | if (error) { 23 | console.log('fail update'); 24 | } else { 25 | console.log('success update'); 26 | } 27 | }); 28 | 29 | grid.gridView.render({ fetch: true }); 30 | -------------------------------------------------------------------------------- /demos/radio-selection/company-name.jade: -------------------------------------------------------------------------------- 1 | i 2 | span=model[property] 3 | -------------------------------------------------------------------------------- /demos/radio-selection/demo.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "./index.jade" 3 | } 4 | -------------------------------------------------------------------------------- /demos/radio-selection/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= title 5 | body 6 | .container 7 | .row 8 | .grid-root 9 | .pager-root 10 | script(type="application/javascript", src=bundle) 11 | -------------------------------------------------------------------------------- /demos/radio-selection/index.js: -------------------------------------------------------------------------------- 1 | var pgrid = require('projection-grid'); 2 | var Customer = require('./js-data-resource'); 3 | var keyHeaderTemplate = require('./key-column-header.jade'); 4 | var companyNameTemplate = require('./company-name.jade'); 5 | require('bootstrap-webpack'); 6 | 7 | var grid = pgrid.factory().create({ 8 | el: '.grid-root', 9 | dataSource: { 10 | type: 'js-data', 11 | resource: Customer, 12 | }, 13 | selectable: 'single', 14 | hideHeaders: true, 15 | pageable: { 16 | pageSize: 10, 17 | pageSizes: [5, 10, 15, 20], 18 | }, 19 | columns: [ 20 | { 21 | name: 'CustomerID', 22 | title: 'Customer ID', 23 | sortable: true, 24 | locked: true, 25 | headerTemplate: keyHeaderTemplate, 26 | }, 27 | { 28 | name: 'CompanyName', 29 | title: 'Company Name', 30 | sortable: true, 31 | template: companyNameTemplate, 32 | attributes: { 33 | class: 'company-name-cell', 34 | }, 35 | headerAttributes: { 36 | class: 'company-name-header', 37 | }, 38 | }, 39 | { 40 | name: 'City', 41 | title: 'City', 42 | sortable: true, 43 | }, 44 | { 45 | name: 'Contact', 46 | field: 'ContactName', 47 | value: item => `${item.ContactName} (${item.ContactTitle})`, 48 | sortable: true, 49 | }, 50 | ], 51 | pagerView: { 52 | el: '.pager-root', 53 | availablePageSizes: [5, 10, 15, 20], 54 | }, 55 | }); 56 | 57 | grid.gridView.render({ fetch: true }); 58 | -------------------------------------------------------------------------------- /demos/radio-selection/js-data-resource.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var JSData = require('js-data'); 3 | var DSHttpAdapter = require('js-data-http'); 4 | 5 | class ODataAdapter extends DSHttpAdapter { 6 | 7 | constructor(options) { 8 | super(_.extend(options, { 9 | queryTransform(definition, params) { 10 | var query = { $count: true }; 11 | 12 | _.has(params, 'offset') && (query.$skip = params.offset); 13 | _.has(params, 'limit') && (query.$top = params.limit); 14 | _.has(params, 'orderBy') && (query.$orderby = params.orderBy.map(function (item) { 15 | return item[0] + ' ' + item[1].toLowerCase(); 16 | }).join(',')); 17 | 18 | return query; 19 | }, 20 | })); 21 | } 22 | 23 | findAll(definition, params, options) { 24 | let totalCount = 0; 25 | 26 | options.afterFindAll = (resource, data) => { 27 | totalCount = data['@odata.count']; 28 | return data.value; 29 | }; 30 | options.afterInject = (resource, instances) => { 31 | Object.defineProperty(instances, 'totalCount', { value: totalCount }); 32 | }; 33 | 34 | return super.findAll(definition, params, options); 35 | } 36 | } 37 | 38 | JSData.DSUtils.Promise = require('bluebird'); 39 | 40 | var store = new JSData.DS(); 41 | store.registerAdapter('odata', new ODataAdapter({ 42 | basePath: 'http://services.odata.org/V4/Northwind/Northwind.svc/', 43 | }), { default: true }); 44 | 45 | module.exports = store.defineResource({ 46 | name: 'Customers', 47 | idAttribute: 'CustomerID', 48 | }); 49 | -------------------------------------------------------------------------------- /demos/radio-selection/key-column-header.jade: -------------------------------------------------------------------------------- 1 | i 2 | span=$text 3 | -------------------------------------------------------------------------------- /demos/vnext/columns/README.md: -------------------------------------------------------------------------------- 1 | # Configuration for columns 2 | -------------------------------------------------------------------------------- /demos/vnext/data-source/README.md: -------------------------------------------------------------------------------- 1 | # Configuration for data-source 2 | -------------------------------------------------------------------------------- /demos/vnext/data-source/custom/demo.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "./index.jade" 3 | } 4 | -------------------------------------------------------------------------------- /demos/vnext/data-source/custom/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= title 5 | body(style="padding-top: 70px") 6 | nav.navbar.navbar-default.navbar-fixed-top 7 | .container-fluid.navbar-container 8 | .navbar-header 9 | .navbar-brand Projection Grid vNext 10 | h1 JSData Data Source 11 | p. 12 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 13 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 14 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 15 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 16 | velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint 17 | occaecat cupidatat non proident, sunt in culpa qui officia deserunt 18 | mollit anim id est laborum. 19 | .grid-container 20 | 21 | script(type="application/javascript", src=bundle) 22 | -------------------------------------------------------------------------------- /demos/vnext/data-source/custom/index.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | 3 | import pgrid from '../../../../js'; 4 | import store from './js-data-resource.js'; 5 | 6 | import './index.less'; 7 | import 'bootstrap-webpack'; 8 | 9 | // This is a decorator of the OData data source 10 | // It filter out the items with CustomerID starts with 'A'. 11 | class CustomDataSource extends pgrid.dataSource.JSData { 12 | constructor(resource, options) { 13 | super(resource, options); 14 | this._jsdataDS = new pgrid.dataSource.JSData(resource); 15 | } 16 | 17 | query(options) { 18 | return this._jsdataDS.query(options).then(({ 19 | totalCount, 20 | items, 21 | }) => { 22 | const filteredItems = _.filter(items, item => item.CustomerID[0] !== 'A'); 23 | 24 | return { 25 | totalCount, 26 | items: filteredItems, 27 | }; 28 | }); 29 | } 30 | } 31 | 32 | window.gridView = pgrid.factory({ vnext: true }).create({ 33 | el: '.grid-container', 34 | tableClasses: ['table', 'table-bordered'], 35 | scrolling: { 36 | virtualized: true, 37 | }, 38 | dataSource: new CustomDataSource(store), 39 | }).gridView.render(); 40 | -------------------------------------------------------------------------------- /demos/vnext/data-source/custom/index.less: -------------------------------------------------------------------------------- 1 | .grid-viewport { 2 | height: 600px; 3 | width: 900px; 4 | overflow-y: auto; 5 | position: relative; 6 | border-style: dashed; 7 | 8 | ul.nav.nav-tabs { 9 | position: absolute; 10 | top: 0; 11 | left: 0; 12 | } 13 | } 14 | 15 | table.table { 16 | thead { 17 | background: lavender; 18 | } 19 | } 20 | 21 | col { 22 | width: 120px; 23 | } 24 | -------------------------------------------------------------------------------- /demos/vnext/data-source/custom/js-data-resource.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var JSData = require('js-data'); 3 | var DSHttpAdapter = require('js-data-http'); 4 | 5 | JSData.DSUtils.Promise = require('bluebird'); 6 | 7 | var store = new JSData.DS(); 8 | store.registerAdapter('http', new DSHttpAdapter({ 9 | basePath: 'http://services.odata.org/V4/Northwind/Northwind.svc/', 10 | deserialize: function (definition, response) { 11 | return response.data.value; 12 | }, 13 | queryTransform: function (definition, params) { 14 | var query = {}; 15 | 16 | _.has(params, 'offset') && (query.$skip = params.offset); 17 | _.has(params, 'limit') && (query.$top = params.limit); 18 | _.has(params, 'orderBy') && (query.$orderby = params.orderBy.map(function (item) { 19 | return item[0] + ' ' + item[1].toLowerCase(); 20 | }).join(',')); 21 | 22 | return query; 23 | }, 24 | }), { default: true }); 25 | 26 | module.exports = store.defineResource({ 27 | name: 'Customers', 28 | idAttribute: 'CustomerID', 29 | }); 30 | -------------------------------------------------------------------------------- /demos/vnext/data-source/jsdata/demo.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "./index.jade" 3 | } 4 | -------------------------------------------------------------------------------- /demos/vnext/data-source/jsdata/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= title 5 | body(style="padding-top: 70px") 6 | nav.navbar.navbar-default.navbar-fixed-top 7 | .container-fluid.navbar-container 8 | .navbar-header 9 | .navbar-brand Projection Grid vNext 10 | h1 JSData Data Source 11 | p. 12 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 13 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 14 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 15 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 16 | velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint 17 | occaecat cupidatat non proident, sunt in culpa qui officia deserunt 18 | mollit anim id est laborum. 19 | .grid-container 20 | 21 | script(type="application/javascript", src=bundle) 22 | -------------------------------------------------------------------------------- /demos/vnext/data-source/jsdata/index.js: -------------------------------------------------------------------------------- 1 | import pgrid from '../../../../js'; 2 | import store from './js-data-resource.js'; 3 | 4 | import './index.less'; 5 | import 'bootstrap-webpack'; 6 | 7 | window.gridView = pgrid.factory({ vnext: true }).create({ 8 | el: '.grid-container', 9 | tableClasses: ['table', 'table-bordered'], 10 | scrolling: { 11 | virtualized: true, 12 | }, 13 | dataSource: { 14 | type: 'jsdata', 15 | entity: store, 16 | }, 17 | }).gridView.render(); 18 | -------------------------------------------------------------------------------- /demos/vnext/data-source/jsdata/index.less: -------------------------------------------------------------------------------- 1 | .grid-viewport { 2 | height: 600px; 3 | width: 900px; 4 | overflow-y: auto; 5 | position: relative; 6 | border-style: dashed; 7 | 8 | ul.nav.nav-tabs { 9 | position: absolute; 10 | top: 0; 11 | left: 0; 12 | } 13 | } 14 | 15 | table.table { 16 | thead { 17 | background: lavender; 18 | } 19 | } 20 | 21 | col { 22 | width: 120px; 23 | } 24 | -------------------------------------------------------------------------------- /demos/vnext/data-source/jsdata/js-data-resource.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var JSData = require('js-data'); 3 | var DSHttpAdapter = require('js-data-http'); 4 | 5 | JSData.DSUtils.Promise = require('bluebird'); 6 | 7 | var store = new JSData.DS(); 8 | store.registerAdapter('http', new DSHttpAdapter({ 9 | basePath: 'http://services.odata.org/V4/Northwind/Northwind.svc/', 10 | deserialize: function (definition, response) { 11 | return response.data.value; 12 | }, 13 | queryTransform: function (definition, params) { 14 | var query = {}; 15 | 16 | _.has(params, 'offset') && (query.$skip = params.offset); 17 | _.has(params, 'limit') && (query.$top = params.limit); 18 | _.has(params, 'orderBy') && (query.$orderby = params.orderBy.map(function (item) { 19 | return item[0] + ' ' + item[1].toLowerCase(); 20 | }).join(',')); 21 | 22 | return query; 23 | }, 24 | }), { default: true }); 25 | 26 | module.exports = store.defineResource({ 27 | name: 'Customers', 28 | idAttribute: 'CustomerID', 29 | }); 30 | -------------------------------------------------------------------------------- /demos/vnext/data-source/memory/demo.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "./index.jade" 3 | } 4 | -------------------------------------------------------------------------------- /demos/vnext/data-source/memory/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= title 5 | body(style="padding-top: 70px") 6 | nav.navbar.navbar-default.navbar-fixed-top 7 | .container-fluid.navbar-container 8 | .navbar-header 9 | .navbar-brand Projection Grid vNext 10 | h1 Memory Data Source 11 | p. 12 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 13 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 14 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 15 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 16 | velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint 17 | occaecat cupidatat non proident, sunt in culpa qui officia deserunt 18 | mollit anim id est laborum. 19 | .grid-container 20 | 21 | script(type="application/javascript", src=bundle) 22 | -------------------------------------------------------------------------------- /demos/vnext/data-source/memory/index.js: -------------------------------------------------------------------------------- 1 | import pgrid from '../../../../js'; 2 | import people from './people.json'; 3 | 4 | import './index.less'; 5 | import 'bootstrap-webpack'; 6 | 7 | window.gridView = pgrid.factory({ vnext: true }).create({ 8 | el: '.grid-container', 9 | tableClasses: ['table', 'table-bordered'], 10 | scrolling: { 11 | virtualized: true, 12 | }, 13 | dataSource: { 14 | type: 'memory', 15 | data: people.value, 16 | primaryKey: 'UserName', 17 | }, 18 | }).gridView.render(); 19 | -------------------------------------------------------------------------------- /demos/vnext/data-source/memory/index.less: -------------------------------------------------------------------------------- 1 | .grid-viewport { 2 | height: 600px; 3 | width: 900px; 4 | overflow-y: auto; 5 | position: relative; 6 | border-style: dashed; 7 | 8 | ul.nav.nav-tabs { 9 | position: absolute; 10 | top: 0; 11 | left: 0; 12 | } 13 | } 14 | 15 | table.table { 16 | thead { 17 | background: lavender; 18 | } 19 | } 20 | 21 | col { 22 | width: 120px; 23 | } 24 | -------------------------------------------------------------------------------- /demos/vnext/data-source/odata/demo.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "./index.jade" 3 | } 4 | -------------------------------------------------------------------------------- /demos/vnext/data-source/odata/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= title 5 | body(style="padding-top: 70px") 6 | nav.navbar.navbar-default.navbar-fixed-top 7 | .container-fluid.navbar-container 8 | .navbar-header 9 | .navbar-brand Projection Grid vNext 10 | h1 OData Data Source 11 | p. 12 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 13 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 14 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 15 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 16 | velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint 17 | occaecat cupidatat non proident, sunt in culpa qui officia deserunt 18 | mollit anim id est laborum. 19 | .grid-container 20 | 21 | script(type="application/javascript", src=bundle) 22 | -------------------------------------------------------------------------------- /demos/vnext/data-source/odata/index.js: -------------------------------------------------------------------------------- 1 | import pgrid from '../../../../js'; 2 | 3 | import './index.less'; 4 | import 'bootstrap-webpack'; 5 | 6 | window.gridView = pgrid.factory({ vnext: true }).create({ 7 | el: '.grid-container', 8 | tableClasses: ['table', 'table-bordered'], 9 | scrolling: { 10 | virtualized: true, 11 | }, 12 | dataSource: { 13 | type: 'odata', 14 | url: 'http://services.odata.org/V4/Northwind/Northwind.svc/Orders', 15 | primaryKey: 'OrderID', 16 | }, 17 | }).gridView.render(); 18 | -------------------------------------------------------------------------------- /demos/vnext/data-source/odata/index.less: -------------------------------------------------------------------------------- 1 | .grid-viewport { 2 | height: 600px; 3 | width: 900px; 4 | overflow-y: auto; 5 | position: relative; 6 | border-style: dashed; 7 | 8 | ul.nav.nav-tabs { 9 | position: absolute; 10 | top: 0; 11 | left: 0; 12 | } 13 | } 14 | 15 | table.table { 16 | thead { 17 | background: lavender; 18 | } 19 | } 20 | 21 | col { 22 | width: 120px; 23 | } 24 | -------------------------------------------------------------------------------- /demos/vnext/events/README.md: -------------------------------------------------------------------------------- 1 | # Handling Grid Events 2 | -------------------------------------------------------------------------------- /demos/vnext/events/demo.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "./index.jade" 3 | } -------------------------------------------------------------------------------- /demos/vnext/events/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= title 5 | body(style="padding-top: 70px") 6 | nav.navbar.navbar-default.navbar-fixed-top 7 | .container-fluid.navbar-container 8 | .navbar-header 9 | .navbar-brand Projection Grid vNext 10 | h1 Events 11 | .grid-container 12 | script(type="application/javascript", src=bundle) 13 | -------------------------------------------------------------------------------- /demos/vnext/events/index.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import _ from 'underscore'; 3 | import Backbone from 'backbone'; 4 | import pgrid from '../../../js'; 5 | 6 | import './index.less'; 7 | import 'bootstrap-webpack'; 8 | 9 | window.gridView = pgrid.factory({ vnext: true }).create({ 10 | el: '.grid-container', 11 | tableClasses: ['table', 'table-bordered'], 12 | scrolling: { 13 | virtualized: true, 14 | header: { 15 | type: 'sticky', 16 | offset() { 17 | return $('.navbar-container').height(); 18 | }, 19 | }, 20 | }, 21 | dataSource: { 22 | type: 'odata', 23 | url: 'http://services.odata.org/V4/Northwind/Northwind.svc/Orders', 24 | primaryKey: 'OrderID', 25 | }, 26 | selection: true, 27 | }).gridView.render(); 28 | 29 | gridView.on('willReload', () => { 30 | console.log('Will reload'); 31 | }); 32 | 33 | gridView.on('didReload', () => { 34 | console.log('Did reload'); 35 | }); 36 | 37 | gridView.on('willRedraw', () => { 38 | console.log('Will redraw'); 39 | }); 40 | 41 | gridView.on('didRedraw', () => { 42 | console.log('Did redraw'); 43 | }); 44 | 45 | gridView.on('willUpdate', (changes) => { 46 | console.log(`Will update with changes ${_.keys(changes)}`); 47 | }); 48 | 49 | gridView.on('didUpdate', (changes) => { 50 | console.log(`Did update with changes ${_.keys(changes)}`); 51 | }); 52 | 53 | gridView.on('willSelect', (selections) => { 54 | console.log(`Will select ${selections[0]}`); 55 | }); 56 | 57 | gridView.on('didSelect', (selections) => { 58 | console.log(`Did select ${selections[0]}`); 59 | }); 60 | 61 | -------------------------------------------------------------------------------- /demos/vnext/events/index.less: -------------------------------------------------------------------------------- 1 | table.table { 2 | max-width: none; 3 | word-wrap: break-word; 4 | 5 | col { 6 | width: 120px; 7 | } 8 | 9 | thead { 10 | background: lavender; 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /demos/vnext/pagination/demo.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "./index.jade" 3 | } 4 | -------------------------------------------------------------------------------- /demos/vnext/pagination/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= title 5 | body(style="padding-top: 70px") 6 | nav.navbar.navbar-default.navbar-fixed-top 7 | .container-fluid.navbar-container 8 | .navbar-header 9 | .navbar-brand Pagination 10 | h1.title-window-viewport Window Viewport (data source: memory) 11 | .container-window-viewport 12 | .container-window-pagination 13 | 14 | script(type="application/javascript", src=bundle) 15 | -------------------------------------------------------------------------------- /demos/vnext/pagination/index.js: -------------------------------------------------------------------------------- 1 | import Backbone from 'backbone'; 2 | import pgrid from '../../../js'; 3 | import pagerViewPlugin from './pager-view-plugin.js'; 4 | 5 | import './index.less'; 6 | import 'bootstrap-webpack'; 7 | 8 | class CustomView extends Backbone.View { 9 | events() { 10 | return { 11 | 'click h2': () => console.log('click'), 12 | }; 13 | } 14 | render() { 15 | this.$el.html('

Custom View

'); 16 | return this; 17 | } 18 | } 19 | 20 | window.customView = new CustomView().render(); 21 | 22 | window.gridView = pgrid.factory({ vnext: true }).use(pagerViewPlugin).create({ 23 | el: '.container-window-viewport', 24 | viewport: '.container-window-viewport', 25 | stickyHeader: true, 26 | virtualized: true, 27 | dataSource: { 28 | type: 'odata', 29 | url: 'http://services.odata.org/V4/Northwind/Northwind.svc/Orders', 30 | primaryKey: 'OrderID', 31 | }, 32 | selection: true, 33 | columns: [{ 34 | name: 'Group 0', 35 | html: 'Group 0', 36 | columns: [{ 37 | name: 'CustomerID', 38 | sortable: true, 39 | },{ 40 | name: 'OrderID', 41 | sortable: -1, 42 | }, { 43 | name: 'ShipAddress', 44 | property: 'ShipAddress/length', 45 | title: 'Ship Address Length', 46 | width: 150, 47 | sortable: 'length(ShipAddress)', 48 | }, { 49 | name: 'Destination', 50 | property: item => `${item.ShipCountry} / ${item.ShipCity}`, 51 | }], 52 | },{ 53 | name: 'ShipCity', 54 | sortable: true, 55 | }], 56 | events: { 57 | 'click th.column-header': (e) => console.log(e.target), 58 | }, 59 | pagerView: { 60 | el: '.container-window-pagination', 61 | availablePageSizes: [5,10,20], 62 | pageSize: 20, 63 | }, 64 | }).gridView.render(); 65 | -------------------------------------------------------------------------------- /demos/vnext/pagination/index.less: -------------------------------------------------------------------------------- 1 | .grid-container { 2 | width: 1200px; 3 | 4 | .viewport { 5 | height: 600px; 6 | } 7 | } 8 | 9 | table.table { 10 | thead { 11 | background: lavender; 12 | } 13 | } 14 | 15 | col { 16 | width: 120px; 17 | } 18 | 19 | tr.row-buffer-changed { 20 | background: LightPink; 21 | } 22 | tr.row-buffer-committed { 23 | background: LightGreen; 24 | } 25 | -------------------------------------------------------------------------------- /demos/vnext/pagination/pager-view-plugin.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import { PaginationView } from 'pagination-control'; 3 | 4 | export default definePlugin => definePlugin('pagerView', [ 5 | 'gridView', 6 | ], function (gridView) { 7 | const pagerViewConfig = gridView.get('pagerView'); 8 | const page = new PaginationView(pagerViewConfig).render(); 9 | const initPageSize = pagerViewConfig.pageSize || pagerViewConfig.availablePageSizes[0]; 10 | const initPageNumber = pagerViewConfig.pageNumber || 0; 11 | gridView.set({ query: _.defaults({ skip: initPageSize * initPageNumber, take: initPageSize }, gridView.get('query')) }); 12 | gridView.on('didUpdate', () => { 13 | page.itemCount = gridView.totalCountRows; 14 | }); 15 | page.on('change:page-number', pageNumber => { 16 | page.pageNumber = pageNumber; 17 | gridView.set({ query: _.defaults({ skip: page.pageSize * pageNumber, take: page.pageSize }, gridView.get('query')) }); 18 | }); 19 | page.on('change:page-size', pageSize => { 20 | page.pageSize = pageSize; 21 | gridView.set({ query: _.defaults({ take: pageSize }, gridView.get('query')) }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /demos/vnext/rows/README.md: -------------------------------------------------------------------------------- 1 | # Configuration for rows 2 | -------------------------------------------------------------------------------- /demos/vnext/rows/demo.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "./index.jade" 3 | } -------------------------------------------------------------------------------- /demos/vnext/rows/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= title 5 | body(style="padding-top: 70px") 6 | nav.navbar.navbar-default.navbar-fixed-top 7 | .container-fluid.navbar-container 8 | .navbar-header 9 | .navbar-brand Projection Grid vNext 10 | h1 Row Configuartaion 11 | .grid-container 12 | script(type="application/javascript", src=bundle) 13 | -------------------------------------------------------------------------------- /demos/vnext/rows/index.less: -------------------------------------------------------------------------------- 1 | .grid-container { 2 | width: 1200px; 3 | 4 | .viewport { 5 | height: 600px; 6 | } 7 | } 8 | 9 | table.table { 10 | thead { 11 | background: lavender; 12 | } 13 | max-width: none; 14 | } 15 | 16 | col { 17 | width: 120px; 18 | } 19 | 20 | tr.row-buffer-changed { 21 | background: LightPink; 22 | } 23 | tr.row-buffer-committed { 24 | background: LightGreen; 25 | } 26 | 27 | .red-color { 28 | color: red; 29 | } 30 | .longer { 31 | color: blue; 32 | } 33 | 34 | tr.noborder { 35 | border: hidden; 36 | } 37 | 38 | table { 39 | thead { 40 | tr.separator { 41 | height: 0; 42 | padding: 0; 43 | th { 44 | height: 0; 45 | padding: 0; 46 | }; 47 | }; 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /demos/vnext/scrolling/README.md: -------------------------------------------------------------------------------- 1 | # Configuration for scrolling 2 | -------------------------------------------------------------------------------- /demos/vnext/scrolling/columns.js: -------------------------------------------------------------------------------- 1 | import emailsTemplate from './emails.jade'; 2 | 3 | export default [{ 4 | name: 'UserName', 5 | width: 520, 6 | sortable: true, 7 | }, { 8 | name: 'Name', 9 | property: item => `${item.FirstName}, ${item.LastName}`, 10 | width: 150, 11 | sortable: true, 12 | }, { 13 | name: 'Emails', 14 | template: emailsTemplate, 15 | width: 220, 16 | sortable: item => item.Emails.length, 17 | }, { 18 | name: 'AddressInfo', 19 | columns: [{ 20 | name: 'Address', 21 | property: 'AddressInfo/0/Address', 22 | sortable: true, 23 | }, { 24 | name: 'City', 25 | columns: [{ 26 | name: 'CityName', 27 | property: 'AddressInfo/0/City/Name', 28 | sortable: true, 29 | }, { 30 | name: 'CityCountry', 31 | property: 'AddressInfo/0/City/CountryRegion', 32 | sortable: true, 33 | }, { 34 | name: 'CityRegion', 35 | property: 'AddressInfo/0/City/Region', 36 | sortable: true, 37 | }], 38 | }], 39 | }, { 40 | name: 'Gender', 41 | sortable: true, 42 | }, { 43 | name: 'Concurrency', 44 | width: 200, 45 | sortable: true, 46 | }]; 47 | -------------------------------------------------------------------------------- /demos/vnext/scrolling/emails.jade: -------------------------------------------------------------------------------- 1 | ul 2 | each email in value || [] 3 | li=email 4 | 5 | -------------------------------------------------------------------------------- /demos/vnext/scrolling/fixed-header/demo.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "./index.jade" 3 | } 4 | -------------------------------------------------------------------------------- /demos/vnext/scrolling/fixed-header/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= title 5 | body(style="padding-top: 70px") 6 | nav.navbar.navbar-default.navbar-fixed-top 7 | .container-fluid.navbar-container 8 | .navbar-header 9 | .navbar-brand Projection Grid vNext 10 | h1 Fixed Header 11 | p. 12 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 13 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 14 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 15 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 16 | velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint 17 | occaecat cupidatat non proident, sunt in culpa qui officia deserunt 18 | mollit anim id est laborum. 19 | .grid-container 20 | script(type="application/javascript", src=bundle) 21 | -------------------------------------------------------------------------------- /demos/vnext/scrolling/fixed-header/index.js: -------------------------------------------------------------------------------- 1 | import pgrid from '../../../../js'; 2 | import people from '../people.json'; 3 | import columns from '../columns.js'; 4 | import rows from '../rows'; 5 | 6 | import './index.less'; 7 | import 'bootstrap-webpack'; 8 | 9 | window.gridView = pgrid.factory({ vnext: true }).create({ 10 | el: '.grid-container', 11 | tableClasses: ['table', 'table-bordered'], 12 | scrolling: { 13 | virtualized: true, 14 | header: 'fixed', 15 | }, 16 | dataSource: { 17 | type: 'memory', 18 | data: people.value, 19 | }, 20 | rows, 21 | columns, 22 | }).gridView.render(); 23 | -------------------------------------------------------------------------------- /demos/vnext/scrolling/fixed-header/index.less: -------------------------------------------------------------------------------- 1 | .grid-container { 2 | width: 1200px; 3 | 4 | .viewport { 5 | height: 600px; 6 | } 7 | } 8 | 9 | table.table { 10 | thead { 11 | background: lavender; 12 | } 13 | } 14 | 15 | col { 16 | width: 120px; 17 | } 18 | 19 | tr.row-buffer-changed { 20 | background: LightPink; 21 | } 22 | tr.row-buffer-committed { 23 | background: LightGreen; 24 | } 25 | -------------------------------------------------------------------------------- /demos/vnext/scrolling/rows.js: -------------------------------------------------------------------------------- 1 | import Backbone from 'backbone'; 2 | 3 | class CustomView extends Backbone.View { 4 | events() { 5 | return { 6 | 'click button': () => { 7 | console.log('click'); 8 | this.$el.find('.text').val('test view'); 9 | }, 10 | }; 11 | } 12 | render() { 13 | this.$el.html(`

This is a Backbone View Row.

`); 14 | return this; 15 | } 16 | } 17 | 18 | window.customView = new CustomView().render(); 19 | 20 | export default { 21 | headRows: [ 22 | 'column-header-rows', 23 | ], 24 | footRows: [ 25 | { view: window.customView }, 26 | { html: '

This a html row

' }, 27 | ], 28 | }; 29 | -------------------------------------------------------------------------------- /demos/vnext/scrolling/static-header/demo.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "./index.jade" 3 | } 4 | -------------------------------------------------------------------------------- /demos/vnext/scrolling/static-header/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= title 5 | body(style="padding-top: 70px") 6 | nav.navbar.navbar-default.navbar-fixed-top 7 | .container-fluid.navbar-container 8 | .navbar-header 9 | .navbar-brand Projection Grid vNext 10 | h1 Static Header 11 | p. 12 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 13 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 14 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 15 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 16 | velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint 17 | occaecat cupidatat non proident, sunt in culpa qui officia deserunt 18 | mollit anim id est laborum. 19 | .grid-container 20 | script(type="application/javascript", src=bundle) 21 | -------------------------------------------------------------------------------- /demos/vnext/scrolling/static-header/index.js: -------------------------------------------------------------------------------- 1 | import pgrid from '../../../../js'; 2 | import people from '../people.json'; 3 | import columns from '../columns.js'; 4 | import rows from '../rows'; 5 | 6 | import './index.less'; 7 | import 'bootstrap-webpack'; 8 | 9 | window.gridView = pgrid.factory({ vnext: true }).create({ 10 | el: '.grid-container', 11 | tableClasses: ['table', 'table-bordered'], 12 | scrolling: { 13 | virtualized: true, 14 | header: 'static', 15 | }, 16 | dataSource: { 17 | type: 'memory', 18 | data: people.value, 19 | }, 20 | rows, 21 | columns, 22 | }).gridView.render(); 23 | -------------------------------------------------------------------------------- /demos/vnext/scrolling/static-header/index.less: -------------------------------------------------------------------------------- 1 | .grid-container { 2 | width: 1200px; 3 | } 4 | 5 | table.table { 6 | thead { 7 | background: lavender; 8 | } 9 | } 10 | 11 | col { 12 | width: 120px; 13 | } 14 | 15 | tr.row-buffer-changed { 16 | background: LightPink; 17 | } 18 | tr.row-buffer-committed { 19 | background: LightGreen; 20 | } 21 | -------------------------------------------------------------------------------- /demos/vnext/scrolling/sticky-header-on-element-flex/demo.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "./index.jade" 3 | } 4 | -------------------------------------------------------------------------------- /demos/vnext/scrolling/sticky-header-on-element-flex/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= title 5 | body(style="padding-top: 70px") 6 | nav.navbar.navbar-default.navbar-fixed-top 7 | .container-fluid.navbar-container 8 | .navbar-header 9 | .navbar-brand Projection Grid vNext 10 | .grid-viewport 11 | h1 Sticky Header/Flex layout/Selection - with Element Viewport 12 | p. 13 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 14 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 15 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 16 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 17 | velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint 18 | occaecat cupidatat non proident, sunt in culpa qui officia deserunt 19 | mollit anim id est laborum. 20 | .grid-container 21 | 22 | script(type="application/javascript", src=bundle) 23 | -------------------------------------------------------------------------------- /demos/vnext/scrolling/sticky-header-on-element-flex/index.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import $ from 'jquery'; 3 | import pgrid from '../../../../js'; 4 | import people from '../people.json'; 5 | import rows from '../rows'; 6 | 7 | import './index.less'; 8 | import 'bootstrap-webpack'; 9 | 10 | const columns = [{ 11 | name: 'UserName', 12 | width: 70, 13 | sortable: true, 14 | group: 'freezing', 15 | }, { 16 | name: 'Name', 17 | property: item => `${item.FirstName}, ${item.LastName}`, 18 | width: 70, 19 | sortable: true, 20 | group: 'freezing', 21 | }, { 22 | name: 'Emails', 23 | width: 200, 24 | sortable: item => item.Emails.length, 25 | group: 'freezing', 26 | }, { 27 | name: 'AddressInfo', 28 | width: 100, 29 | columns: [{ 30 | width: 100, 31 | name: 'Address', 32 | property: 'AddressInfo/0/Address', 33 | sortable: true, 34 | }, { 35 | name: 'City', 36 | columns: [{ 37 | width: 200, 38 | name: 'CityName', 39 | property: 'AddressInfo/0/City/Name', 40 | sortable: true, 41 | }, { 42 | width: 250, 43 | name: 'CityCountry', 44 | property: 'AddressInfo/0/City/CountryRegion', 45 | sortable: true, 46 | }], 47 | }], 48 | }]; 49 | 50 | const gridConfigBase = { 51 | tableClasses: ['table'], 52 | layout: 'flex', 53 | selection: true, 54 | dataSource: { 55 | type: 'memory', 56 | data: people.value, 57 | }, 58 | rows, 59 | columns, 60 | }; 61 | 62 | const stickyGridConfig = _.defaults({ 63 | el: '.grid-container', 64 | scrolling: { 65 | virtualized: true, 66 | viewport: $('.grid-viewport'), 67 | header: 'sticky', 68 | }, 69 | }, gridConfigBase); 70 | 71 | window.gridView = pgrid.factory({ vnext: true }).create(stickyGridConfig).gridView; 72 | 73 | _.each([ 74 | 'willRedrawHeader', 75 | 'didRedrawHeader', 76 | 'willRedrawFooter', 77 | 'didRedrawFooter', 78 | 'willRedrawBody', 79 | 'didRedrawBody', 80 | ], event => { 81 | window.gridView.on(event, () => { 82 | window.console.log(`trigger the event ${event}`); 83 | }); 84 | }); 85 | 86 | window.gridView.render(); 87 | -------------------------------------------------------------------------------- /demos/vnext/scrolling/sticky-header-on-element-flex/index.less: -------------------------------------------------------------------------------- 1 | .grid-viewport { 2 | height: 600px; 3 | width: 900px; 4 | overflow-y: auto; 5 | position: relative; 6 | border-style: dashed; 7 | } 8 | 9 | .header .column-header { 10 | border-top: 2px solid #81868B; 11 | border-bottom: 2px solid #81868B; 12 | background-color: #fff; 13 | } 14 | 15 | .freezing-group .th, .freezing-group .td { 16 | background: #fcffe5; 17 | } 18 | .other-group .th, .other-group .td { 19 | background: #edeeff; 20 | } 21 | 22 | .flex-cell { 23 | width: 150px; 24 | } 25 | -------------------------------------------------------------------------------- /demos/vnext/scrolling/sticky-header-on-element/demo.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "./index.jade" 3 | } 4 | -------------------------------------------------------------------------------- /demos/vnext/scrolling/sticky-header-on-element/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= title 5 | body(style="padding-top: 70px") 6 | nav.navbar.navbar-default.navbar-fixed-top 7 | .container-fluid.navbar-container 8 | .navbar-header 9 | .navbar-brand Projection Grid vNext 10 | .grid-viewport 11 | h1 Sticky Header - with Element Viewport 12 | p. 13 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 14 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 15 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 16 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 17 | velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint 18 | occaecat cupidatat non proident, sunt in culpa qui officia deserunt 19 | mollit anim id est laborum. 20 | .grid-container 21 | 22 | script(type="application/javascript", src=bundle) 23 | -------------------------------------------------------------------------------- /demos/vnext/scrolling/sticky-header-on-element/index.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import pgrid from '../../../../js'; 3 | import people from '../people.json'; 4 | import columns from '../columns.js'; 5 | import rows from '../rows'; 6 | 7 | import './index.less'; 8 | import 'bootstrap-webpack'; 9 | 10 | window.gridView = pgrid.factory({ vnext: true }).create({ 11 | el: '.grid-container', 12 | tableClasses: ['table', 'table-bordered'], 13 | scrolling: { 14 | virtualized: true, 15 | viewport: $('.grid-viewport'), 16 | header: 'sticky', 17 | }, 18 | dataSource: { 19 | type: 'memory', 20 | data: people.value, 21 | }, 22 | rows, 23 | columns, 24 | }).gridView.render(); 25 | -------------------------------------------------------------------------------- /demos/vnext/scrolling/sticky-header-on-element/index.less: -------------------------------------------------------------------------------- 1 | .grid-viewport { 2 | height: 600px; 3 | width: 900px; 4 | overflow-y: auto; 5 | position: relative; 6 | border-style: dashed; 7 | 8 | ul.nav.nav-tabs { 9 | position: absolute; 10 | top: 0; 11 | left: 0; 12 | } 13 | } 14 | 15 | table.table { 16 | thead { 17 | background: lavender; 18 | } 19 | } 20 | 21 | col { 22 | width: 120px; 23 | } 24 | -------------------------------------------------------------------------------- /demos/vnext/scrolling/sticky-header-on-window/demo.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "./index.jade" 3 | } 4 | -------------------------------------------------------------------------------- /demos/vnext/scrolling/sticky-header-on-window/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= title 5 | body(style="padding-top: 70px") 6 | nav.navbar.navbar-default.navbar-fixed-top 7 | .container-fluid.navbar-container 8 | .navbar-header 9 | .navbar-brand Projection Grid vNext 10 | h1 Sticky Header - with Window Viewport 11 | p. 12 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 13 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 14 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 15 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 16 | velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint 17 | occaecat cupidatat non proident, sunt in culpa qui officia deserunt 18 | mollit anim id est laborum. 19 | .grid-container 20 | script(type="application/javascript", src=bundle) 21 | -------------------------------------------------------------------------------- /demos/vnext/scrolling/sticky-header-on-window/index.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import pgrid from '../../../../js'; 3 | import people from '../people.json'; 4 | import columns from '../columns.js'; 5 | import rows from '../rows'; 6 | 7 | import './index.less'; 8 | import 'bootstrap-webpack'; 9 | 10 | window.gridView = pgrid.factory({ vnext: true }).create({ 11 | el: '.grid-container', 12 | tableClasses: ['table', 'table-bordered'], 13 | scrolling: { 14 | virtualized: true, 15 | viewport: window, 16 | header: { 17 | type: 'sticky', 18 | offset() { 19 | return $('.navbar-container').height(); 20 | }, 21 | }, 22 | }, 23 | dataSource: { 24 | type: 'memory', 25 | data: people.value, 26 | }, 27 | rows, 28 | columns, 29 | }).gridView.render(); 30 | -------------------------------------------------------------------------------- /demos/vnext/scrolling/sticky-header-on-window/index.less: -------------------------------------------------------------------------------- 1 | table.table { 2 | thead { 3 | background: lavender; 4 | } 5 | } 6 | 7 | col { 8 | width: 120px; 9 | } 10 | -------------------------------------------------------------------------------- /demos/vnext/selection/multiple/demo.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "./index.jade" 3 | } 4 | -------------------------------------------------------------------------------- /demos/vnext/selection/multiple/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= title 5 | body(style="padding-top: 70px") 6 | nav.navbar.navbar-default.navbar-fixed-top 7 | .container-fluid.navbar-container 8 | .navbar-header 9 | .navbar-brand Projection Grid vNext 10 | h1 Multiple Selection 11 | .grid-container 12 | script(type="application/javascript", src=bundle) 13 | -------------------------------------------------------------------------------- /demos/vnext/selection/multiple/index.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import Backbone from 'backbone'; 3 | import pgrid from '../../../../js'; 4 | 5 | import './index.less'; 6 | import 'bootstrap-webpack'; 7 | 8 | window.gridView = pgrid.factory({ vnext: true }).create({ 9 | el: '.grid-container', 10 | tableClasses: ['table', 'table-bordered'], 11 | scrolling: { 12 | virtualized: true, 13 | header: { 14 | type: 'sticky', 15 | offset() { 16 | return $('.navbar-container').height(); 17 | }, 18 | }, 19 | }, 20 | dataSource: { 21 | type: 'odata', 22 | url: 'http://services.odata.org/V4/Northwind/Northwind.svc/Orders', 23 | primaryKey: 'OrderID', 24 | }, 25 | selection: { 26 | a11y: { 27 | selectAllLabel: 'select all label', 28 | }, 29 | selectable(item) { 30 | return item.OrderID && (item.OrderID % 2 === 0); 31 | }, 32 | colClasses: ['selection-column'], 33 | headClasses: ['selection-header'], 34 | bodyClasses: ['selection-cell'], 35 | }, 36 | }).gridView.render(); 37 | 38 | gridView.on('willSelect', (selections) => { 39 | console.log(`Will select ${selections.length} items`); 40 | }); 41 | 42 | gridView.on('didSelect', (selections) => { 43 | console.log(`Did select ${selections.length} items`); 44 | }); 45 | 46 | -------------------------------------------------------------------------------- /demos/vnext/selection/multiple/index.less: -------------------------------------------------------------------------------- 1 | table.table { 2 | max-width: none; 3 | word-wrap: break-word; 4 | 5 | col { 6 | width: 120px; 7 | } 8 | 9 | thead { 10 | background: Lavender; 11 | } 12 | 13 | col.selection-column { 14 | width: 40px; 15 | } 16 | 17 | td.selection-cell { 18 | text-align: center; 19 | background: Lavender; 20 | } 21 | 22 | th.selection-header { 23 | text-align: center; 24 | background: LavenderBlush; 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /demos/vnext/selection/single/demo.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "./index.jade" 3 | } 4 | -------------------------------------------------------------------------------- /demos/vnext/selection/single/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= title 5 | body(style="padding-top: 70px") 6 | nav.navbar.navbar-default.navbar-fixed-top 7 | .container-fluid.navbar-container 8 | .navbar-header 9 | .navbar-brand Projection Grid vNext 10 | h1 Single Selection 11 | .grid-container 12 | script(type="application/javascript", src=bundle) 13 | -------------------------------------------------------------------------------- /demos/vnext/selection/single/index.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import _ from 'underscore'; 3 | import Backbone from 'backbone'; 4 | import pgrid from '../../../../js'; 5 | 6 | import './index.less'; 7 | import 'bootstrap-webpack'; 8 | 9 | window.gridView = pgrid.factory({ vnext: true }).create({ 10 | el: '.grid-container', 11 | tableClasses: ['table', 'table-bordered'], 12 | scrolling: { 13 | virtualized: true, 14 | header: { 15 | type: 'sticky', 16 | offset() { 17 | return $('.navbar-container').height(); 18 | }, 19 | }, 20 | }, 21 | dataSource: { 22 | type: 'odata', 23 | url: 'http://services.odata.org/V4/Northwind/Northwind.svc/Orders', 24 | primaryKey: 'OrderID', 25 | }, 26 | selection: { 27 | single: true, 28 | selectable(item) { 29 | return _.has(item, 'CustomerID') && item.CustomerID.match(/^[A-N]/); 30 | }, 31 | colClasses: ['selection-column'], 32 | headClasses: ['selection-header'], 33 | bodyClasses: ['selection-cell'], 34 | }, 35 | }).gridView.render(); 36 | 37 | gridView.on('willSelect', (selections) => { 38 | console.log(`Will select ${selections[0]}`); 39 | }); 40 | 41 | gridView.on('didSelect', (selections) => { 42 | console.log(`Did select ${selections[0]}`); 43 | }); 44 | 45 | -------------------------------------------------------------------------------- /demos/vnext/selection/single/index.less: -------------------------------------------------------------------------------- 1 | table.table { 2 | max-width: none; 3 | word-wrap: break-word; 4 | 5 | col { 6 | width: 120px; 7 | } 8 | 9 | thead { 10 | background: lavender; 11 | } 12 | 13 | col.selection-column { 14 | width: 40px; 15 | } 16 | 17 | td.selection-cell { 18 | text-align: center; 19 | background: Lavender; 20 | } 21 | 22 | th.selection-header { 23 | text-align: center; 24 | background: LavenderBlush; 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /demos/vnext/sortable-header/README.md: -------------------------------------------------------------------------------- 1 | # Configuration for rows 2 | -------------------------------------------------------------------------------- /demos/vnext/sortable-header/demo.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "./index.jade" 3 | } -------------------------------------------------------------------------------- /demos/vnext/sortable-header/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= title 5 | body(style="padding-top: 70px") 6 | nav.navbar.navbar-default.navbar-fixed-top 7 | .container-fluid.navbar-container 8 | .navbar-header 9 | .navbar-brand Projection Grid vNext 10 | h1 Row Configuartaion 11 | .grid-container 12 | script(type="application/javascript", src=bundle) 13 | -------------------------------------------------------------------------------- /demos/vnext/sortable-header/index.less: -------------------------------------------------------------------------------- 1 | .grid-container { 2 | width: 1200px; 3 | 4 | .viewport { 5 | height: 600px; 6 | } 7 | } 8 | 9 | table.table { 10 | thead { 11 | background: lavender; 12 | } 13 | } 14 | 15 | col { 16 | width: 120px; 17 | } 18 | 19 | tr.row-buffer-changed { 20 | background: LightPink; 21 | } 22 | tr.row-buffer-committed { 23 | background: LightGreen; 24 | } 25 | 26 | .redColor { 27 | color: red; 28 | } 29 | .longer { 30 | color: blue; 31 | } 32 | 33 | tr.noborder { 34 | border: hidden; 35 | } 36 | 37 | table { 38 | thead { 39 | tr.separator { 40 | height: 0; 41 | padding: 0; 42 | th { 43 | height: 0; 44 | padding: 0; 45 | }; 46 | }; 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /demos/vnext/sortable-header/sortable-header-alphabet.jade: -------------------------------------------------------------------------------- 1 | if direction < 0 2 | span.sort-indicator.glyphicon.glyphicon-sort-by-alphabet 3 | else if direction > 0 4 | span.sort-indicator.glyphicon.glyphicon-sort-by-alphabet-alt 5 | 6 | != html 7 | 8 | -------------------------------------------------------------------------------- /demos/vnext/sortable-header/sortable-header-number.jade: -------------------------------------------------------------------------------- 1 | if direction < 0 2 | span.sort-indicator.glyphicon.glyphicon-sort-by-order 3 | else if direction > 0 4 | span.sort-indicator.glyphicon.glyphicon-sort-by-order-alt 5 | 6 | != html 7 | 8 | -------------------------------------------------------------------------------- /demos/vnext/sortable-header/sortable-header.jade: -------------------------------------------------------------------------------- 1 | if direction < 0 2 | span.sort-indicator.glyphicon.glyphicon-sort-by-attributes 3 | else if direction > 0 4 | span.sort-indicator.glyphicon.glyphicon-sort-by-attributes-alt 5 | 6 | != html 7 | 8 | -------------------------------------------------------------------------------- /demos/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | var webpackConfig = require('../webpack.config'); 4 | 5 | webpackConfig.plugins = webpackConfig.plugins || []; 6 | webpackConfig.plugins.push(new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })); 7 | 8 | webpackConfig.module.loaders = webpackConfig.module.loaders.concat([ 9 | { test: /bootstrap[\\\/]js[\\\/]/, loader: 'imports?jQuery=jquery' }, 10 | { test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=application/font-woff' }, 11 | { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=application/octet-stream' }, 12 | { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file' }, 13 | { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=image/svg+xml' }, 14 | ]); 15 | 16 | module.exports = webpackConfig; 17 | -------------------------------------------------------------------------------- /js/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/projection-grid/ff67cea8338e0de5588178781f6bb786715807cb/js/.DS_Store -------------------------------------------------------------------------------- /js/containerBase.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'lib/underscore', 3 | 'lib/jquery', 4 | 'lib/backbone', 5 | ], function (_, $, Backbone) { 6 | var ContainerBase = Backbone.View.extend({ 7 | events: { 8 | scroll: 'onScroll', 9 | resize: 'onResize', 10 | }, 11 | 12 | onScroll: function (e) { 13 | this.trigger('scroll:container', e); 14 | }, 15 | 16 | onResize: function (e) { 17 | this.trigger('resize:container', e); 18 | }, 19 | 20 | offset: function (/* element, scrollTop, scrollLeft */) { 21 | throw new Error("Offset function not implemented"); 22 | }, 23 | }); 24 | 25 | return ContainerBase; 26 | }); 27 | -------------------------------------------------------------------------------- /js/elementContainer.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'lib/underscore', 3 | 'lib/jquery', 4 | 'component/grid/containerBase', 5 | ], function (_, $, ContainerBase) { 6 | var ElementContainer = ContainerBase.extend({ 7 | offset: function (element) { 8 | var position = $(element).position(); 9 | 10 | return { 11 | top: position.top + this.$el.scrollTop(), 12 | left: position.left + this.$el.scrollLeft(), 13 | }; 14 | }, 15 | }); 16 | 17 | ElementContainer.isValidContainer = function (userContainer) { 18 | return ['absolute', 'relative', 'fixed'].indexOf($(userContainer).css('position')) >= 0; 19 | }; 20 | 21 | return ElementContainer; 22 | }); 23 | -------------------------------------------------------------------------------- /js/factory/grid-factory.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import renderersPlugin from './renderers-plugin'; 3 | import projectionPlugin from './projection-plugin'; 4 | import gridViewPlugin from './grid-view-plugin'; 5 | 6 | import dataSourceVNextPlugin from '../vnext/factory/data-source-plugin.js'; 7 | import gridViewVNextPlugin from '../vnext/factory/grid-view-plugin.js'; 8 | 9 | const configPlugin = definePlugin => definePlugin('config', [], function () { 10 | return this.config; 11 | }); 12 | 13 | class GridFactory { 14 | constructor({ 15 | vnext = false, 16 | } = {}) { 17 | this.pluginIndex = {}; 18 | this.plugins = []; 19 | if (vnext) { 20 | this 21 | .use(configPlugin) 22 | .use(dataSourceVNextPlugin) 23 | .use(gridViewVNextPlugin); 24 | } else { 25 | this 26 | .use(configPlugin) 27 | .use(projectionPlugin) 28 | .use(renderersPlugin) 29 | .use(gridViewPlugin); 30 | } 31 | } 32 | 33 | definePlugin(name, deps, callback) { 34 | const plugin = { name, deps, callback }; 35 | 36 | this.pluginIndex[name] = plugin; 37 | this.plugins.push(plugin); 38 | _.each(deps, dep => { 39 | if (!_.has(this.pluginIndex, dep)) { 40 | throw new Error(`unresolved plugin dependency ${name} -> ${dep}`); 41 | } 42 | }); 43 | } 44 | 45 | use(callback) { 46 | callback(this.definePlugin.bind(this)); 47 | return this; 48 | } 49 | 50 | create(config) { 51 | return _.reduce( 52 | this.plugins, 53 | (result, { name, deps, callback }) => 54 | _.extend(result, { 55 | [name]: callback.apply(result, _.map(deps, dep => result[dep])), 56 | }), 57 | { config } 58 | ); 59 | } 60 | 61 | } 62 | 63 | export default function (options) { 64 | return new GridFactory(options); 65 | } 66 | -------------------------------------------------------------------------------- /js/factory/grid-view-plugin.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import GridView from '../grid-view'; 3 | import layout from '../layout/index'; 4 | import { delegateEvents } from './utility'; 5 | 6 | export default definePlugin => definePlugin('gridView', [ 7 | 'config', 8 | 'projection', 9 | 'renderers', 10 | ], function (config, projection, renderers) { 11 | const gridView = new GridView({ 12 | projection, 13 | el: config.el, 14 | container: _.chain(config) 15 | .result('scrollable') 16 | .result('fixedHeader') 17 | .result('container') 18 | .value(), 19 | schema: config.dataSource.schema, 20 | Layout: layout.TableLayout.partial({ 21 | renderers, 22 | template: layout.templates.table, 23 | hideHeaders: config.hideHeaders, 24 | $metadata: config.layoutOptions || {}, 25 | }), 26 | }); 27 | 28 | delegateEvents({ 29 | from: projection, 30 | to: gridView, 31 | events: [ 32 | 'update:beginning', 33 | 'update:finished', 34 | ], 35 | }); 36 | 37 | return gridView; 38 | }); 39 | -------------------------------------------------------------------------------- /js/factory/renderers-plugin.js: -------------------------------------------------------------------------------- 1 | import layout from '../layout/index'; 2 | 3 | export default definePlugin => definePlugin('renderers', [ 4 | 'config', 5 | ], function (config) { 6 | const renderers = []; 7 | 8 | if (config.scrollable) { 9 | if (config.scrollable.virtual) { 10 | renderers.push(layout.renderers.Virtualization); 11 | } 12 | if (config.scrollable.fixedHeader) { 13 | renderers.push(layout.renderers.FixedHeader); 14 | } 15 | } 16 | 17 | return renderers; 18 | }); 19 | -------------------------------------------------------------------------------- /js/factory/utility.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | 3 | export function delegateEvents({ from, to, events }) { 4 | _.each( 5 | events, 6 | event => from.on(event, (...args) => to.trigger(event, ...args)) 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | import factory from './factory/grid-factory'; 2 | import pkg from '../package.json'; 3 | import { 4 | DataSource, 5 | MemoryDataSource, 6 | ODataDataSource, 7 | JSDataDataSource, 8 | } from './vnext/data-source'; 9 | 10 | module.exports = { 11 | version: pkg.version, 12 | GridView: require('./grid-view'), 13 | projections: require('./projection/index'), 14 | layout: require('./layout/index'), 15 | factory, 16 | popupEditorPrompt: require('./popup-editor/index'), 17 | dataSource: { 18 | Base: DataSource, 19 | Memory: MemoryDataSource, 20 | OData: ODataDataSource, 21 | JSData: JSDataDataSource, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /js/layout/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/projection-grid/ff67cea8338e0de5588178781f6bb786715807cb/js/layout/.DS_Store -------------------------------------------------------------------------------- /js/layout/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | TableLayout: require('./table'), 3 | templates: { 4 | table: require('./template/table.jade'), 5 | }, 6 | renderers: require('./renderer/index'), 7 | }; 8 | -------------------------------------------------------------------------------- /js/layout/measure.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'lib/jquery', 3 | 'lib/underscore', 4 | ], function ($, _) { 5 | function viewport(el, container) { 6 | var $el = el ? $(el) : this.$el; 7 | 8 | container = container || this.container; 9 | var $viewport = container.$el; 10 | 11 | var viewportTop = $viewport.scrollTop(); 12 | var viewportBottom = viewportTop + $viewport.height(); 13 | var viewportLeft = $viewport.scrollLeft(); 14 | 15 | var boundsTop = container.offset($el).top; 16 | var boundsBottom = boundsTop + $el.innerHeight(); 17 | // var boundsLeft = $el.offset().left; 18 | 19 | var visibleTop = Math.max(boundsTop, viewportTop); 20 | var visibleBottom = Math.min(boundsBottom, viewportBottom); 21 | // var visibleLeft = Math.max(boundsLeft, viewportLeft); 22 | 23 | return { 24 | boundsTop: boundsTop, 25 | viewportTop: viewportTop, 26 | top: visibleTop - boundsTop, 27 | bottom: visibleBottom - boundsTop, 28 | offsetLeft: viewportLeft, 29 | }; 30 | } 31 | 32 | function dimensions(el) { 33 | var $el = el ? $(el) : this.$el; 34 | 35 | // calculate heights 36 | // a. header 37 | var ret = { 38 | rows: [], 39 | thead: $el.find('thead > tr').outerHeight(), 40 | }; 41 | 42 | // b. keep row info 43 | $el.find('tbody').children('tr').each(function () { 44 | ret.rows.push($(this).outerHeight()); 45 | }); 46 | 47 | // c. update average row height 48 | var avg = _.reduce(ret.rows, function (memo, num) { 49 | return memo + num; 50 | }, 0) / (ret.rows.length === 0 ? 1 : ret.rows.length); 51 | 52 | ret.avgRowHeight = avg; 53 | ret.estimateHeight = (_.size(this.data.value) * avg) + ret.thead; 54 | 55 | return ret; 56 | } 57 | 58 | function sample() { 59 | // a. render test pass 60 | var $tmpEl = $('
'); 61 | var sample = _.first(this.data.value, 20); 62 | 63 | this.$el.append($tmpEl); 64 | 65 | $tmpEl[0].innerHTML = this.toHTML({ rows: sample }); 66 | 67 | // b. take measures 68 | var ret = dimensions.call(this, $tmpEl); 69 | 70 | // c. clean-up 71 | $tmpEl.remove(); 72 | 73 | return ret; 74 | } 75 | 76 | return { 77 | viewport: viewport, 78 | dimensions: dimensions, 79 | sample: sample, 80 | }; 81 | }); 82 | -------------------------------------------------------------------------------- /js/layout/px.js: -------------------------------------------------------------------------------- 1 | // todo [akamel] move to /component 2 | define(['lib/underscore'], function (_) { 3 | function parse(a) { 4 | return abs(parseFloat(a)); 5 | } 6 | 7 | function abs(a) { 8 | return _.isFinite(a) ? a : 0; 9 | } 10 | 11 | function pixelify(a) { 12 | return abs(a) + 'px'; 13 | } 14 | 15 | return { 16 | parse: parse, 17 | pixelify: pixelify, 18 | }; 19 | }); 20 | -------------------------------------------------------------------------------- /js/layout/renderer/index.js: -------------------------------------------------------------------------------- 1 | import FixedHeaderRenderer from './fixed-header'; 2 | import VirtualizationRenderer from './virtualization'; 3 | 4 | module.exports = { 5 | FixedHeader: FixedHeaderRenderer, 6 | Virtualization: VirtualizationRenderer, 7 | }; 8 | 9 | -------------------------------------------------------------------------------- /js/layout/template/editable.jade: -------------------------------------------------------------------------------- 1 | .grid-edit-icon(title=tooltipText, class=classes) 2 | !=$html 3 | =text 4 | -------------------------------------------------------------------------------- /js/layout/template/row.editable.string.jade: -------------------------------------------------------------------------------- 1 | input.grid-text-input(type="text" value=defaultValue style="width:100%") -------------------------------------------------------------------------------- /js/layout/template/row.tri-state-checked.jade: -------------------------------------------------------------------------------- 1 | - var classes = [].concat(checkState === 'indeterminate' ? ['glyphicon', 'glyphicon-minus'] : checkState === 'checked' ? ['glyphicon', 'glyphicon-ok'] : []) 2 | .column-tri-state-checkbox 3 | span(class=classes) 4 | -------------------------------------------------------------------------------- /js/layout/template/selectable.jade: -------------------------------------------------------------------------------- 1 | input.column-selection(type=type, checked=checked, disabled=disabled, aria-labelledby=labelledId, aria-label=labelString) 2 | -------------------------------------------------------------------------------- /js/layout/template/table.dot: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{~it.columns :column:j}} 5 | 6 | {{~}} 7 | 8 | 9 | 10 | {{~it.value :row:i}} 11 | 12 | {{~it.columns :column:j}} 13 | 16 | {{~}} 17 | 18 | {{~}} 19 | 20 |
{{=column.$text || column.property || column}}
14 | {{= row[column.property].$html || row[column.property] || '' }} 15 |
-------------------------------------------------------------------------------- /js/layout/template/table.mobile.jade: -------------------------------------------------------------------------------- 1 | mixin th(column, cls) 2 | - var cls = cls || []; 3 | if column.header 4 | - cls.push(column.header['class']); 5 | if column.sortable 6 | - cls.push('sortable'); 7 | if column.$orderby 8 | - cls.push('orderby'); 9 | 10 | - cls = cls.join(' '); 11 | th(class=cls) 12 | if column.$orderby 13 | if column.$orderby.dir > 0 14 | span.grid-asc 15 | else 16 | span.grid-des 17 | if (column && column.$html) 18 | != column.$html 19 | else 20 | = (typeof column.$text != 'undefined')? column.$text : (column.property || column) 21 | 22 | mixin td(row, column, cls) 23 | - var cls = cls || []; 24 | if column.cell 25 | - cls.push(column.cell['class']); 26 | - cls = cls.join(' '); 27 | td(class=cls) 28 | - var res = row[column.property] 29 | if (res && res.$html) 30 | != res.$html 31 | else 32 | = res 33 | 34 | .table-responsive 35 | table.table.table-hover.grid 36 | thead 37 | tr 38 | each column, index in locals['columns.group.lock'][true] || [] 39 | +th(column, ['lock']) 40 | each column, index in locals['columns.group.lock'][false] || [] 41 | +th(column) 42 | if ((locals['columns.group.lock'][false] || []).length ==- 0) 43 | th.fillter 44 | tbody 45 | each row, i in value 46 | tr 47 | each column, j in locals['columns.group.lock'][true] || [] 48 | +td(row, column, ['lock']) 49 | each column, j in locals['columns.group.lock'][false] || [] 50 | +td(row, column) 51 | if ((locals['columns.group.lock'][false] || []).length ==- 0) 52 | td.fillterr 53 | -------------------------------------------------------------------------------- /js/layout/template/table.while.jade: -------------------------------------------------------------------------------- 1 | table.table.table-striped.table-hover 2 | - var i = 0; 3 | - var j = 0; 4 | thead 5 | tr 6 | - j = 0; 7 | while j < columns.length 8 | - var column = columns[j]; 9 | th(style=column.$th_style) 10 | = column.$text || column.property || column 11 | - j++ 12 | tbody 13 | - i = 0; 14 | while i < value.length 15 | - var row = value[i]; 16 | tr 17 | - j = 0 18 | while j < columns.length 19 | - var column = columns[j]; 20 | td(class=column.$lock?'table-col-lock':undefined, style=column.$td_style) 21 | - var res = row[column.property] 22 | if (res && res.$html) 23 | != res.$html 24 | else 25 | = res 26 | - j++ 27 | - i++ -------------------------------------------------------------------------------- /js/model/index.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'lib/underscore', 3 | 'lib/backbone', 4 | ], function (_, Backbone) { 5 | return Backbone.Model.extend({ 6 | defaults: { 7 | layout: undefined, 8 | projection: undefined, 9 | }, 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /js/model/response.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'lib/underscore', 3 | 'lib/backbone', 4 | ], function (_, Backbone) { 5 | return Backbone.Model.extend({ 6 | defaults: { 7 | value: [], 8 | select: null, 9 | count: 0, 10 | aggregate: [], 11 | }, 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /js/popup-editor/index.jade: -------------------------------------------------------------------------------- 1 | form.form-inline.grid-popup-editor(role='dialog') 2 | input.form-control.editor(type='text', value=value) 3 |   4 | button.btn.btn-primary.save(type='button')=saveButtonText 5 |   6 | button.btn.btn-default.cancel(type='button')=cancelButtonText 7 | -------------------------------------------------------------------------------- /js/projection/a11y.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'lib/underscore', 3 | 'lib/backbone', 4 | 'component/grid/projection/base', 5 | ], function (_, Backbone, BaseProjection /* , schemaProperties, Response */) { 6 | const defaultselectAllLabel = 'Select All'; 7 | var Model = BaseProjection.extend({ 8 | defaults: { 9 | 'a11y.selection.selectAllLabel': 'Select All' 10 | }, 11 | name: 'a11y', 12 | update: function (options) { 13 | var selectAllLabel = this.get('a11y.selection.selectAllLabel'); 14 | if (Model.__super__.update.call(this, options)) { 15 | this.patch({ 16 | 'a11y.selection.uniqueId': _.uniqueId().concat('-'), 17 | 'a11y.selection.selectAllLabel': _.isString(selectAllLabel) ? selectAllLabel : defaultselectAllLabel, 18 | }); 19 | } 20 | }, 21 | }); 22 | return Model; 23 | }); 24 | -------------------------------------------------------------------------------- /js/projection/aggregate-row.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'lib/underscore', 3 | 'lib/backbone', 4 | 'component/grid/projection/base', 5 | ], function (_, Backbone, BaseProjection) { 6 | var createRows = function (fn, data) { 7 | var rows = fn(data); 8 | 9 | _.each(rows, function (row) { 10 | row.$metadata = _.extend({}, row.$metadata, { type: 'aggregate' }); 11 | }); 12 | 13 | return rows; 14 | }; 15 | 16 | var Model = BaseProjection.extend({ 17 | 18 | defaults: { 19 | 'aggregate.top': null, 20 | 'aggregate.bottom': null, 21 | }, 22 | 23 | name: 'aggregate-row', 24 | 25 | update: function (options) { 26 | var value, topFn, rowTop, bottomFn, rowBottom; 27 | 28 | if (Model.__super__.update.call(this, options)) { 29 | value = this.src.data.get('value'); 30 | 31 | if (value) { 32 | topFn = this.get('aggregate.top'); 33 | bottomFn = this.get('aggregate.bottom'); 34 | rowTop = _.isFunction(topFn) ? createRows(topFn, this.src.data) : null; 35 | rowBottom = _.isFunction(bottomFn) ? createRows(bottomFn, this.src.data) : null; 36 | 37 | if (rowTop) { 38 | value = _.flatten(rowTop).concat(value); 39 | } 40 | 41 | if (rowBottom) { 42 | value = value.concat(_.flatten(rowBottom)); 43 | } 44 | 45 | this.patch({ 46 | value: value, 47 | }); 48 | } 49 | } 50 | }, 51 | 52 | }); 53 | 54 | return Model; 55 | }); 56 | -------------------------------------------------------------------------------- /js/projection/column-hovertext.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'lib/underscore', 3 | 'lib/backbone', 4 | 'component/grid/projection/base', 5 | ], function (_, Backbone, BaseProjection) { 6 | var Model = BaseProjection.extend({ 7 | name: 'column-hovertext', 8 | update: function (options) { 9 | if (Model.__super__.update.call(this, options)) { 10 | var model = this.src.data; 11 | var columns = model.get('columns'); 12 | 13 | _.each(columns, function(column) { 14 | column.$metadata = _.result(column, '$metadata', {}); 15 | column.$metadata['attr.head'] = _.result(column.$metadata, 'attr.head', {}); 16 | column.$metadata['attr.head'].title = _.result(column.config, 'hoverText') || _.result(column.config, 'title') || ''; 17 | }); 18 | 19 | this.patch({ columns: columns }); 20 | } 21 | }, 22 | }); 23 | 24 | return Model; 25 | }); 26 | -------------------------------------------------------------------------------- /js/projection/column-i18n.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'lib/underscore', 3 | 'lib/backbone', 4 | 'component/grid/projection/base', 5 | 'component/grid/schema/properties', 6 | 'component/grid/model/response', 7 | ], function (_, Backbone, BaseProjection /* , schemaProperties, Response */) { 8 | var Model = BaseProjection.extend({ 9 | defaults: { 10 | 'column.i18n': { 11 | '': function (name) { 12 | return name; 13 | }, 14 | }, 15 | 'subColumn.i18n': {}, 16 | }, 17 | name: 'column-i18n', 18 | beforeSet: function (local) { 19 | if (_.has(local, 'column.i18n')) { 20 | if (!_.isObject(local['column.i18n'])) { 21 | local['column.i18n'] = this.defaults['column.i18n']; 22 | } 23 | } 24 | }, 25 | update: function (options) { 26 | // todo [akamel] when calling a deep update; suppress onchange event based updates 27 | // Model.__super__.update.call(this, options); 28 | 29 | if (Model.__super__.update.call(this, options)) { 30 | var model = this.src.data; 31 | var colOptions = this.get('column.i18n'); 32 | var subColOptions = this.get('subColumn.i18n'); 33 | var columns = model.get('columns') || {}; 34 | var $default = colOptions['']; 35 | 36 | var i18nColumns = {}; 37 | _.each(_.keys(columns), function (element) { 38 | var opt = colOptions[element]; 39 | if (_.isUndefined(opt)) { 40 | opt = $default; 41 | } 42 | 43 | i18nColumns[element] = _.defaults({ 44 | $text: _.isFunction(opt) ? opt(element) : opt, 45 | property: element, 46 | }, columns[element]); 47 | 48 | i18nColumns[element].config = i18nColumns[element].config || {}; 49 | i18nColumns[element].config.subColTitle = subColOptions[element]; 50 | }); 51 | 52 | this.patch({ 53 | columns: i18nColumns, 54 | }); 55 | } else { 56 | // todo [akamel] unset our properties only 57 | // this.unset(); 58 | } 59 | }, 60 | }); 61 | 62 | return Model; 63 | }); 64 | -------------------------------------------------------------------------------- /js/projection/column-queryable.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'lib/underscore', 3 | 'lib/backbone', 4 | 'component/grid/projection/base', 5 | 'component/grid/schema/properties', 6 | 'component/grid/model/response', 7 | ], function (_, Backbone, BaseProjection /* , schemaProperties, Response */) { 8 | var Model = BaseProjection.extend({ 9 | defaults: { 10 | 'column.skip': 0, 11 | 'column.take': Number.MAX_VALUE, 12 | 'column.lock': [], 13 | 'column.filter': function () { 14 | return true; 15 | }, 16 | 'column.in': undefined, 17 | }, 18 | name: 'column-queryable', 19 | update: function (options) { 20 | if (Model.__super__.update.call(this, options)) { 21 | var model = this.src.data; 22 | var take = this.get('column.take'); 23 | var skip = this.get('column.skip'); 24 | var lock = this.get('column.lock') || []; 25 | var filter = this.get('column.filter'); 26 | // todo [akamel] consider renaming to column.select 27 | var $in = this.get('column.in'); 28 | var select = _.size(model.get('columns')) ? _.map(model.get('columns'), function (i) { 29 | return i.property; 30 | }) : model.get('select'); 31 | var unlocked = _.isFunction(filter) ? _.filter($in || select, filter) : ($in || select); 32 | var lookup = model.get('columns'); 33 | var set = _.chain(unlocked).difference(lock).value(); 34 | var col = set; 35 | 36 | if (!_.isNumber(take)) { 37 | take = Number.MAX_VALUE; 38 | } 39 | 40 | take = Math.max(take - _.size(lock), 0); 41 | if (_.size(set) < skip) { 42 | skip = 0; 43 | // this.set({ 'columns.skip' : 0 }, { silent : true }); 44 | } 45 | 46 | // start query 47 | var skipped = _.first(set, skip); 48 | 49 | if (skip) { 50 | col = _.rest(set, skip); 51 | } 52 | 53 | var remaining = _.rest(col, take); 54 | 55 | col = _.union(lock, _.first(col, take)); 56 | // end query 57 | 58 | _.each(col, function (element) { 59 | if (!lookup[element]) { 60 | lookup[element] = { property: element }; 61 | } 62 | lookup[element].$lock = _.contains(lock, element); 63 | }); 64 | 65 | this.patch({ 66 | 'select': col, 67 | // todo [akamel] rename to column.in??? 68 | // , 'columns.select' : set 69 | 'columns.skipped': skipped, 70 | 'columns.remaining': remaining, 71 | // , 'columns.count' : _.size(res) 72 | // todo [akamel] do we still need to update skip? 73 | 'column.skip': skip, 74 | }); 75 | } else { 76 | // todo [akamel] unset our properties only 77 | // this.unset(); 78 | } 79 | }, 80 | }); 81 | 82 | return Model; 83 | }); 84 | -------------------------------------------------------------------------------- /js/projection/column-template.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'lib/underscore', 3 | 'component/grid/projection/base', 4 | ], function (_, BaseProjection) { 5 | 'use strict'; 6 | 7 | var Model = BaseProjection.extend({ 8 | defaults: { 9 | 'column.template': {}, 10 | }, 11 | name: 'column-template', 12 | update: function (options) { 13 | // todo [akamel] when calling a deep update; suppress onchange event based updates 14 | // Model.__super__.update.call(this, options); 15 | 16 | // TODO [imang]: columns: ideally we should not need to read from select. 17 | if (Model.__super__.update.call(this, options)) { 18 | var model = this.src.data; 19 | var colTemplate = this.get('column.template'); 20 | var columns = model.get('columns'); 21 | _.each(columns, function (item, property) { 22 | var ret = _.clone(item); 23 | var templateValue = colTemplate[property]; 24 | 25 | if (_.has(colTemplate, property)) { 26 | ret.$html = _.isFunction(templateValue) ? templateValue(ret) : templateValue; 27 | } 28 | 29 | columns[property] = ret; 30 | }); 31 | 32 | this.patch({ columns: columns }); 33 | } else { 34 | // todo [akamel] unset our properties only 35 | // this.unset(); 36 | } 37 | }, 38 | }); 39 | 40 | return Model; 41 | }); 42 | -------------------------------------------------------------------------------- /js/projection/columns.js: -------------------------------------------------------------------------------- 1 | import BaseProjection from './base'; 2 | 3 | const ColumnsProjection = BaseProjection.extend({ 4 | defaults: { 5 | columns: {}, 6 | }, 7 | 8 | name: 'columns', 9 | 10 | update(options) { 11 | if (BaseProjection.prototype.update.call(this, options)) { 12 | this.patch({ 13 | columns: this.get('columns'), 14 | }); 15 | } 16 | }, 17 | 18 | }); 19 | 20 | export default ColumnsProjection; 21 | -------------------------------------------------------------------------------- /js/projection/editable-string.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Important!!! 4 | * 5 | * The initial check-in is for unblock the simple grid in advanced campaign creation and 6 | * may cause bugs if you try to use it with some more complex grid. Please DO NOT use this 7 | * projection until we update this and remove the comments here. 8 | * 9 | * todo [yucongj, wewei] add the model to maintail the input data. 10 | * 11 | */ 12 | 13 | define([ 14 | 'lib/underscore', 15 | 'lib/jquery', 16 | 'component/grid/projection/base', 17 | 'component/grid/layout/template/row.editable.string.jade', 18 | ], function (_, jquery, BaseProjection, editableTemplate) { 19 | 'use strict'; 20 | 21 | var Model = BaseProjection.extend({ 22 | defaults: { 23 | 'column.editable.string': {}, 24 | }, 25 | name: 'column-editable-string', 26 | update: function (options) { 27 | if (Model.__super__.update.call(this, options)) { 28 | var model = this.src.data; 29 | var columnEditable = this.get('column.editable.string'); 30 | var value = _.map(model.get('value'), function (item) { 31 | var ret = _.clone(item); 32 | 33 | _.each(columnEditable, function (value, key) { 34 | if (_.has(ret, key)) { 35 | if (!_.isObject(ret[key])) { 36 | var obj = new Object(ret[key]); //eslint-disable-line 37 | 38 | if (_.isUndefined(ret[key])) { 39 | obj.$undefined = true; 40 | } 41 | 42 | if (_.isNull(ret[key])) { 43 | obj.$null = true; 44 | } 45 | 46 | ret[key] = obj; 47 | } 48 | 49 | var defaultValue = (ret[key].$undefined || ret[key].$null) ? value.defaultValue : ret[key]; 50 | 51 | ret[key].$html = editableTemplate({ defaultValue: defaultValue }); 52 | } 53 | }); 54 | 55 | return ret; 56 | }); 57 | 58 | this.patch({ value: value }); 59 | } 60 | }, 61 | }); 62 | 63 | return Model; 64 | }); 65 | -------------------------------------------------------------------------------- /js/projection/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | A11y: require('./a11y'), 3 | AggregateRow: require('./aggregate-row'), 4 | Base: require('./base'), 5 | ColumnI18n: require('./column-i18n'), 6 | ColumnQueryable: require('./column-queryable'), 7 | ColumnShifter: require('./column-shifter'), 8 | ColumnTemplate: require('./column-template'), 9 | EditableString: require('./editable-string'), 10 | Editable: require('./editable'), 11 | JSData: require('./jsdata'), 12 | Map: require('./map'), 13 | MemoryQueryable: require('./memory-queryable'), 14 | Memory: require('./memory'), 15 | Mock: require('./mock'), 16 | Odata: require('./odata'), 17 | Page: require('./page'), 18 | PropertyTemplate: require('./property-template'), 19 | RowCheckbox: require('./row-checkbox'), 20 | Row: require('./row'), 21 | RowTriStateCheckboxProjection: require('./row-tri-state-checkbox'), 22 | RowIndex: require('./row-index'), 23 | Sink: require('./sink'), 24 | // Todo[hezhan]: deprecated? 25 | // ColumnGroup: require('./column-group'), 26 | ColumnHoverText: require('./column-hovertext'), 27 | Columns: require('./columns').default, 28 | }; 29 | -------------------------------------------------------------------------------- /js/projection/jsdata.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'bluebird', 3 | 'lib/underscore', 4 | 'component/grid/projection/base', 5 | 'component/grid/schema/properties.js', 6 | ], function (Promise, _, BaseProjection, schemaProperties) { 7 | var Model = BaseProjection.extend({ 8 | defaults: { 9 | 'jsdata.query': undefined, 10 | 'jsdata.entity': undefined, 11 | 'jsdata.options': undefined, 12 | 'skip': undefined, 13 | 'take': undefined, 14 | 'filter': undefined, 15 | 'orderby': [], 16 | 'select': [], 17 | }, 18 | name: 'jsdata', 19 | 20 | update: function () { 21 | var entity = this.get('jsdata.entity'); 22 | var options = _.defaults(this.get('jsdata.options'), { all: true }); 23 | var op = {}; 24 | 25 | this.p$fetchData || this.trigger('update:beginning'); 26 | 27 | var take = this.get('take'); 28 | 29 | if (take) { 30 | op.limit = take; 31 | } 32 | 33 | var skip = this.get('skip'); 34 | 35 | if (skip) { 36 | op.offset = skip; 37 | } 38 | 39 | var filter = this.get('filter'); 40 | 41 | if (filter) { 42 | op.where = filter; 43 | } 44 | 45 | var query = this.get('jsdata.query'); 46 | 47 | if (query) { 48 | op.query = query; 49 | } 50 | 51 | var orderby = this.get('orderby'); 52 | 53 | if (orderby && orderby.length) { 54 | op.orderBy = _.reduce(orderby, function (arr, obj) { 55 | _.each(obj, function (value, key) { 56 | arr.push([key, value > 0 ? 'ASC' : 'DESC']); 57 | }); 58 | return arr; 59 | }, []); 60 | } 61 | 62 | var p$fetchData = this.p$fetchData = entity.findAll(op, options) 63 | .then(function (data) { 64 | if (this.p$fetchData === p$fetchData) { 65 | var delta = { 66 | value: data, 67 | count: data.totalCount, 68 | select: schemaProperties.from(data), 69 | error: undefined, 70 | }; 71 | if (_.has(data, 'raw')) { 72 | delta.rawValue = data.raw; 73 | } 74 | this.patch(delta); 75 | } 76 | }.bind(this)) 77 | .catch(function (error) { 78 | if (this.p$fetchData === p$fetchData) { 79 | this.patch({ error }); 80 | } 81 | }.bind(this)) 82 | .finally(function () { 83 | if (this.p$fetchData === p$fetchData) { 84 | this.trigger('update:finished', this.data.get('error')); 85 | this.p$fetchData = null; 86 | } 87 | }.bind(this)); 88 | }, 89 | }); 90 | 91 | return Model; 92 | }); 93 | -------------------------------------------------------------------------------- /js/projection/map.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'lib/underscore', 3 | 'lib/backbone', 4 | 'component/grid/projection/base', 5 | 'component/grid/schema/properties', 6 | 'component/grid/model/response', 7 | ], function (_, Backbone, BaseProjection, schemaProperties /* , Response */) { 8 | var Model = BaseProjection.extend({ 9 | defaults: { 10 | // todo [akamel] consider supporting a select on this level? 11 | map: _.identity, 12 | }, 13 | name: 'map', 14 | update: function (options) { 15 | // Model.__super__.update.call(this, options); 16 | 17 | if (Model.__super__.update.call(this, options)) { 18 | var model = this.src.data; 19 | var map = this.get('map'); 20 | var value = _.isFunction(map) ? _.map(model.get('value'), map) : model.get('value'); 21 | 22 | value = _.flatten(value); 23 | 24 | this.patch({ 25 | value: value, 26 | select: _.without(schemaProperties.from(value), '$metadata'), 27 | }); 28 | } else { 29 | // todo [akamel] unset our properties only 30 | // this.unset(); 31 | } 32 | }, 33 | }); 34 | 35 | return Model; 36 | }); 37 | -------------------------------------------------------------------------------- /js/projection/memory-filter.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'lib/underscore', 3 | 'lib/backbone', 4 | 'component/grid/projection/base', 5 | 'component/grid/schema/properties', 6 | 'component/grid/model/response', 7 | ], function (_, Backbone, BaseProjection, schemaProperties /* , Response */) { 8 | var Model = BaseProjection.extend({ 9 | defaults: { 10 | filter: function () { 11 | return true; 12 | }, 13 | select: [], 14 | }, 15 | name: 'memory-filter', 16 | beforeSet: function (local) { 17 | if (_.has(local, 'filter')) { 18 | if (!_.isFunction(local.filter)) { 19 | local.filter = this.defaults.filter; 20 | } 21 | } 22 | }, 23 | update: function (options) { 24 | if (Model.__super__.update.call(this, options)) { 25 | var model = this.src.data; 26 | 27 | var value = model.get('value'); 28 | 29 | value = _.chain(value) 30 | .filter(this.get('filter')) 31 | .value(); 32 | 33 | var select = this.get('select'); 34 | if (!_.size(select)) { 35 | select = schemaProperties.from(value); 36 | } 37 | 38 | this.patch({ 39 | value: value, 40 | select: select, 41 | }); 42 | } 43 | }, 44 | }); 45 | 46 | return Model; 47 | }); 48 | -------------------------------------------------------------------------------- /js/projection/memory-page.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'lib/underscore', 3 | 'lib/backbone', 4 | 'component/grid/projection/base', 5 | 'component/grid/schema/properties', 6 | 'component/grid/model/response', 7 | ], function (_, Backbone, BaseProjection, schemaProperties /* , Response */) { 8 | var Model = BaseProjection.extend({ 9 | defaults: { 10 | skip: 0, 11 | take: Number.MAX_VALUE, 12 | select: [], 13 | }, 14 | name: 'memory-page', 15 | update: function (options) { 16 | if (Model.__super__.update.call(this, options)) { 17 | var model = this.src.data; 18 | 19 | var value = model.get('value'); 20 | 21 | value = _.chain(value) 22 | .rest(this.get('skip')) 23 | .first(this.get('take')) 24 | .value(); 25 | 26 | var select = this.get('select'); 27 | if (!_.size(select)) { 28 | select = schemaProperties.from(value); 29 | } 30 | 31 | this.patch({ 32 | value: value, 33 | select: select, 34 | }); 35 | } 36 | }, 37 | }); 38 | 39 | return Model; 40 | }); 41 | -------------------------------------------------------------------------------- /js/projection/memory-queryable.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'lib/underscore', 3 | 'lib/backbone', 4 | 'component/grid/projection/base', 5 | 'component/grid/schema/properties', 6 | 'component/grid/model/response', 7 | ], function (_, Backbone, BaseProjection, schemaProperties /* , Response */) { 8 | var Model = BaseProjection.extend({ 9 | defaults: { 10 | 'skip': 0, 11 | 'take': Number.MAX_VALUE, 12 | 'filter': function () { 13 | return true; 14 | }, 15 | 'orderby': [], 16 | 'select': [], 17 | 'column.sortable': {}, 18 | }, 19 | name: 'map-queryable', 20 | beforeSet: function (local) { 21 | if (_.has(local, 'filter')) { 22 | if (!_.isFunction(local.filter)) { 23 | local.filter = this.defaults.filter; 24 | } 25 | } 26 | }, 27 | update: function (options) { 28 | // Model.__super__.update.call(this, options); 29 | 30 | if (Model.__super__.update.call(this, options)) { 31 | var model = this.src.data; 32 | var order = _.chain(this.get('orderby')).first().pairs().first().value(); 33 | 34 | var orderKey = _.first(order); 35 | var orderDir = _.last(order); 36 | 37 | var value = _.chain(model.get('value')).filter(this.get('filter')); 38 | var sortFunc = this.get('column.sortable')[orderKey]; 39 | 40 | if (orderKey) { 41 | if (_.isFunction(sortFunc)) { 42 | value = _.chain(sortFunc(value.value(), orderDir)); 43 | } else { 44 | value = value.sortBy(orderKey); 45 | if (orderDir === -1) { 46 | value = value.reverse(); 47 | } 48 | } 49 | } 50 | 51 | value = value 52 | .rest(this.get('skip')) 53 | .first(this.get('take')) 54 | .value(); 55 | 56 | var select = this.get('select'); 57 | if (!_.size(select)) { 58 | select = schemaProperties.from(value); 59 | } 60 | 61 | this.patch({ 62 | value: value, 63 | select: select, 64 | }); 65 | } else { 66 | // todo [akamel] unset our properties only 67 | // this.unset(); 68 | } 69 | }, 70 | }); 71 | 72 | return Model; 73 | }); 74 | -------------------------------------------------------------------------------- /js/projection/memory-sort.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'lib/underscore', 3 | 'lib/backbone', 4 | 'component/grid/projection/base', 5 | 'component/grid/schema/properties', 6 | 'component/grid/model/response', 7 | ], function (_, Backbone, BaseProjection, schemaProperties /* , Response */) { 8 | var Model = BaseProjection.extend({ 9 | defaults: { 10 | 'orderby': [], 11 | 'select': [], 12 | 'column.sortable': {}, 13 | }, 14 | name: 'memory-sort', 15 | update: function (options) { 16 | if (Model.__super__.update.call(this, options)) { 17 | var model = this.src.data; 18 | var order = _.chain(this.get('orderby')) 19 | .first() 20 | .pairs() 21 | .first() 22 | .value(); 23 | 24 | var orderKey = _.first(order); 25 | var orderDir = _.last(order); 26 | 27 | var value = model.get('value'); 28 | 29 | var sortFunc = this.get('column.sortable')[orderKey]; 30 | 31 | if (orderKey) { 32 | if (_.isFunction(sortFunc)) { 33 | value = sortFunc(value, orderDir); 34 | } else { 35 | value = _.sortBy(value, orderKey); 36 | if (orderDir === -1) { 37 | value = value.reverse(); 38 | } 39 | } 40 | } 41 | 42 | var select = this.get('select'); 43 | if (!_.size(select)) { 44 | select = schemaProperties.from(value); 45 | } 46 | 47 | this.patch({ 48 | value: value, 49 | select: select, 50 | }); 51 | } else { 52 | // TODO [akamel] unset our properties only 53 | // this.unset(); 54 | } 55 | }, 56 | }); 57 | 58 | return Model; 59 | }); 60 | -------------------------------------------------------------------------------- /js/projection/memory.js: -------------------------------------------------------------------------------- 1 | define(['component/grid/projection/base'], function (BaseProjection) { 2 | var Model = BaseProjection.extend({ 3 | defaults: { 4 | seed: [], 5 | }, 6 | update: function () { 7 | this.trigger('update:beginning'); 8 | 9 | var value = this.get('seed'); 10 | 11 | this.data.set({ 12 | value: value, 13 | count: value.length, 14 | }); 15 | 16 | this.trigger('update:finished'); 17 | }, 18 | }); 19 | 20 | return Model; 21 | }); 22 | -------------------------------------------------------------------------------- /js/projection/mock.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'lib/underscore', 3 | 'lib/backbone', 4 | 'component/grid/projection/base', 5 | ], function (_, Backbone, BaseProjection) { 6 | var wrds = ['troubles', 'kahlua', 'poncho', 'suzie', 'baheyya']; 7 | var idx = 0; 8 | 9 | function randomRow() { 10 | var wrd1 = wrds[_.random(0, wrds.length - 1)]; 11 | var wrd2 = wrds[_.random(0, wrds.length - 1)]; 12 | 13 | return { 14 | index: idx++, 15 | name: wrd1 + ' ' + wrd2, 16 | age: _.random(0, 22), 17 | }; 18 | } 19 | 20 | var Model = BaseProjection.extend({ 21 | defaults: { 22 | n: 5000, 23 | }, 24 | name: 'mock', 25 | update: function () { 26 | this.trigger('update:beginning'); 27 | var value = []; 28 | 29 | _(this.get('n')).times(function () { 30 | value.push(randomRow()); 31 | }); 32 | 33 | this.data.set({ 34 | value: value, 35 | count: value.length, 36 | }); 37 | this.trigger('update:finished'); 38 | }, 39 | }); 40 | 41 | return Model; 42 | }); 43 | -------------------------------------------------------------------------------- /js/projection/odata.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'bluebird', 3 | 'lib/underscore', 4 | 'lib/backbone', 5 | 'lib/jquery', 6 | 'component/grid/projection/base', 7 | 'component/grid/schema/properties', 8 | ], function (Promise, _, Backbone, $, BaseProjection, schemaProperties) { 9 | var Model = BaseProjection.extend({ 10 | defaults: { 11 | verb: 'get', 12 | url: undefined, 13 | skip: undefined, 14 | take: undefined, 15 | filter: undefined, 16 | orderby: [], 17 | select: [], 18 | }, 19 | name: 'odata', 20 | update: function () { 21 | this.p$fetchData || this.trigger('update:beginning'); 22 | 23 | var url = this.get('url'); 24 | 25 | url = _.isFunction(url) ? url() : url; 26 | var op = { 27 | url: url, 28 | $format: 'json', 29 | // todo [akamel] this is odata v3 specific 30 | $count: true, 31 | }; 32 | 33 | var take = this.get('take'); 34 | if (take) { 35 | op.$top = take; 36 | } 37 | 38 | var skip = this.get('skip'); 39 | if (skip) { 40 | op.$skip = skip; 41 | } 42 | 43 | // todo [akamel] only supports one order column 44 | var orderby = this.get('orderby'); 45 | if (_.size(orderby)) { 46 | var col = _.first(orderby); 47 | var key = _.keys(col)[0]; 48 | var dir = col[key]; 49 | 50 | op.$orderby = key + ' ' + (dir > 0 ? 'asc' : 'desc'); 51 | } 52 | 53 | var p$fetchData = this.p$fetchData = new Promise(function (resolve, reject) { 54 | $.getJSON(op.url, _.omit(op, 'url')) 55 | .success(resolve) 56 | .error(function (jqXHR, textStatus, errorThrown) { 57 | reject(new Error(errorThrown)); 58 | }); 59 | }).then(function (data) { 60 | if (p$fetchData === this.p$fetchData) { 61 | var delta = { 62 | value: data.value, 63 | rawValue: data, 64 | select: schemaProperties.from(data.value), 65 | count: data['@odata.count'], 66 | error: undefined, 67 | }; 68 | this.patch(delta); 69 | } 70 | }.bind(this)).catch(function (error) { 71 | if (p$fetchData === this.p$fetchData) { 72 | this.patch({ error: error }); 73 | } 74 | }.bind(this)).finally(function () { 75 | if (p$fetchData === this.p$fetchData) { 76 | this.trigger('update:finished', this.data.get('error')); 77 | this.p$fetchData = null; 78 | } 79 | }.bind(this)); 80 | }, 81 | }); 82 | 83 | return Model; 84 | }); 85 | -------------------------------------------------------------------------------- /js/projection/page.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'lib/underscore', 3 | 'lib/backbone', 4 | 'component/grid/projection/base', 5 | 'component/grid/schema/properties', 6 | 'component/grid/model/response', 7 | ], function (_, Backbone, BaseProjection /* , schemaProperties, Response */) { 8 | var Model = BaseProjection.extend({ 9 | defaults: { 10 | 'page.size': 20, 11 | 'page.number': 0, // zero based 12 | }, 13 | name: 'page', 14 | // todo [akamel] what if we piped after the data was set? 15 | beforeSet: function (local, other) { 16 | var size = _.has(local, 'page.size') ? local['page.size'] : this.get('page.size'); 17 | var number = _.has(local, 'page.number') ? local['page.number'] : this.get('page.number'); 18 | 19 | // todo [akamel] sanetize size and number here 20 | size = Math.max(size, 0); 21 | number = Math.max(number, 0); 22 | 23 | _.extend(other, { 24 | take: size, 25 | skip: size * number, 26 | }); 27 | }, 28 | update: function (options) { 29 | var model = this.src.data; 30 | var size = Math.max(this.get('page.size'), 0); 31 | var count = Math.max(1, model.get('count')); 32 | var number = Math.max(this.get('page.number'), 0); 33 | var pageCount = Math.ceil(count / size); 34 | var pageNumber = Math.min(number, pageCount - 1); 35 | 36 | options = options || {}; 37 | 38 | if (options.deep) { 39 | if (this.src) { 40 | this.src.set({ 41 | take: size, 42 | skip: size * pageNumber, 43 | }, { silent: true }); 44 | } 45 | } 46 | 47 | // Model.__super__.update.call(this, options); 48 | 49 | // if we came in with an update:deep 50 | if (Model.__super__.update.call(this, options)) { 51 | this.patch({ 'page.count': pageCount }); 52 | } else { 53 | // todo [akamel] unset our properties only 54 | // this.unset(); 55 | } 56 | }, 57 | }); 58 | 59 | return Model; 60 | }); 61 | -------------------------------------------------------------------------------- /js/projection/property-template.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'lib/underscore', 3 | 'lib/backbone', 4 | 'component/grid/projection/base', 5 | 'component/grid/schema/properties', 6 | 'component/grid/model/response', 7 | ], function (_, Backbone, BaseProjection /* , schemaProperties, Response */) { 8 | var Model = BaseProjection.extend({ 9 | defaults: { 10 | 'property.template': {}, 11 | }, 12 | name: 'property-template', 13 | update: function (options) { 14 | // Model.__super__.update.call(this, options); 15 | 16 | if (Model.__super__.update.call(this, options)) { 17 | var model = this.src.data; 18 | var opt = this.get('property.template'); 19 | var value = _.map(model.get('value'), function (item) { 20 | var ret = _.clone(item); 21 | 22 | _.each(opt, function (value, key) { 23 | if (_.has(ret, key)) { 24 | var res = value({ 25 | model: item, 26 | property: key, 27 | }); 28 | 29 | if (!_.isObject(ret[key])) { 30 | var obj = new Object(ret[key]); // eslint-disable-line 31 | 32 | if (_.isUndefined(ret[key])) { 33 | obj.$undefined = true; 34 | } 35 | 36 | if (_.isNull(ret[key])) { 37 | obj.$null = true; 38 | } 39 | 40 | ret[key] = obj; 41 | } 42 | 43 | ret[key].$html = res; 44 | } 45 | }); 46 | 47 | return ret; 48 | }); 49 | 50 | this.patch({ value: value }); 51 | } else { 52 | // todo [akamel] unset our properties only 53 | // this.unset(); 54 | } 55 | }, 56 | }); 57 | 58 | return Model; 59 | }); 60 | -------------------------------------------------------------------------------- /js/projection/row-index.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'lib/underscore', 3 | 'lib/backbone', 4 | 'component/grid/projection/base', 5 | 'component/grid/schema/properties', 6 | 'component/grid/model/response', 7 | ], function (_, Backbone, BaseProjection /* , schemaProperties, Response */) { 8 | var Model = BaseProjection.extend({ 9 | defaults: {}, 10 | name: 'row-index', 11 | update: function (options) { 12 | if (Model.__super__.update.call(this, options)) { 13 | var model = this.src.data; 14 | var skip = this.get('skip'); 15 | var value = _.map(model.get('value'), function (i, idx) { 16 | var ret = _.clone(i); 17 | ret.rowIndex = idx + (_.isFinite(skip) ? skip : 0) + 1; 18 | return ret; 19 | }); 20 | 21 | this.patch({ value: value }); 22 | } else { 23 | // todo [akamel] unset our properties only 24 | // this.unset(); 25 | } 26 | }, 27 | }); 28 | 29 | return Model; 30 | }); 31 | -------------------------------------------------------------------------------- /js/projection/row.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'lib/underscore', 3 | 'lib/backbone', 4 | 'component/grid/projection/base', 5 | ], function (_, Backbone, BaseProjection) { 6 | var Model = BaseProjection.extend({ 7 | defaults: { 8 | 'row.classes': {}, 9 | }, 10 | name: 'row', 11 | update: function (options) { 12 | if (Model.__super__.update.call(this, options)) { 13 | var selectedMap = {}; 14 | 15 | _.each(this.get('row.check.list'), function(selectedIndex) { 16 | selectedMap[selectedIndex] = true; 17 | }); 18 | 19 | var model = this.src.data; 20 | var rows = model.get('value'); 21 | 22 | _.each(rows, row => { 23 | var type = _.chain(row).result('$metadata').result('type').value(); 24 | 25 | if (_.isUndefined(type)) { 26 | row.$metadata = _.extend({}, row.$metadata, { 27 | type: 'row', 28 | }); 29 | } 30 | }); 31 | 32 | _.each(rows, (row) => { 33 | var classesRule = this.get('row.classes'); 34 | var checkId = this.get('row.check.id'); 35 | var checkboxAllow = model.get('row.check.allow'); 36 | var classArr = []; 37 | 38 | if (selectedMap[row[checkId]]) { 39 | classArr.push('row-selected'); 40 | } 41 | 42 | var originClass = _.chain(row) 43 | .result('$metadata') 44 | .result('attr') 45 | .result('class') 46 | .value(); 47 | 48 | if (originClass) { 49 | classArr.push(originClass); 50 | } 51 | 52 | 53 | _.each(classesRule, (func, key) => { 54 | var type = _.chain(row).result('$metadata').result('type').value(); 55 | 56 | if (_.isFunction(func) && func(row, type)) { 57 | classArr.push(key); 58 | } 59 | }); 60 | 61 | //attr info from meta 62 | var originId = _.chain(row) 63 | .result('$metadate') 64 | .result('attr') 65 | .result('id') 66 | .value(); 67 | 68 | if (_.isFunction(checkboxAllow) ? checkboxAllow(row) : checkboxAllow) { 69 | var uniqueId = model.get('a11y.selection.uniqueId'); 70 | var id = row[checkId] || originId; 71 | var a11yId = uniqueId.concat(id); 72 | var role = 'row'; 73 | } 74 | 75 | _.extend(row, { 76 | $metadata: { 77 | attr: _.pick({ 78 | class: _.flatten(classArr).join(' '), 79 | id: a11yId, 80 | role: role, 81 | }, Boolean), 82 | }, 83 | }); 84 | }); 85 | 86 | this.patch({ value: rows }); 87 | } 88 | }, 89 | }); 90 | 91 | return Model; 92 | }); 93 | -------------------------------------------------------------------------------- /js/projection/sink.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'lib/underscore', 3 | 'component/grid/projection/base', 4 | 'component/grid/schema/properties', 5 | ], function (_, BaseProjection, schemaProperties) { 6 | var Model = BaseProjection.extend({ 7 | 8 | defaults: { 9 | seed: null, 10 | }, 11 | 12 | name: 'sink', 13 | 14 | update: function () { 15 | var value; 16 | 17 | value = this.get('seed'); 18 | 19 | if (value) { 20 | var select = schemaProperties.from(value); 21 | this.patch({ 22 | value: value, 23 | select: select, 24 | count: _.size(value), 25 | }); 26 | } 27 | }, 28 | 29 | }); 30 | 31 | return Model; 32 | }); 33 | -------------------------------------------------------------------------------- /js/schema/list-view.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/projection-grid/ff67cea8338e0de5588178781f6bb786715807cb/js/schema/list-view.js -------------------------------------------------------------------------------- /js/schema/properties.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'lib/underscore', 3 | ], function (_) { 4 | function from(arr) { 5 | var obj = _.first(arr); 6 | 7 | return _.keys(obj || {}); 8 | } 9 | 10 | return { from: from }; 11 | }); 12 | -------------------------------------------------------------------------------- /js/vnext/data-source/index.js: -------------------------------------------------------------------------------- 1 | export { DataSource } from './base.js'; 2 | export { ODataDataSource } from './odata.js'; 3 | export { JSDataDataSource } from './js-data.js'; 4 | export { MemoryDataSource } from './memory.js'; 5 | 6 | -------------------------------------------------------------------------------- /js/vnext/data-source/js-data.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import { DataSource } from './base.js'; 3 | 4 | function translateOrderBy(dataSource, orderByParams) { 5 | const orderBy = _.chain(dataSource.normalizeOrderBy(orderByParams)) 6 | .map(([key, direction]) => [ 7 | key, 8 | direction > 0 ? 'ASC' : 'DESC', 9 | ]) 10 | .value(); 11 | 12 | return _.isEmpty(orderBy) ? null : orderBy; 13 | } 14 | 15 | function translateParams(dataSource, params) { 16 | return _.chain(params) 17 | .omit('skip', 'take', 'filter', 'orderby') 18 | .extend(_.pick({ 19 | offset: params.skip, 20 | limit: params.take, 21 | where: params.filter, 22 | orderBy: translateOrderBy(dataSource, params.orderby), 23 | }, Boolean)) 24 | .value(); 25 | } 26 | 27 | /** 28 | * Data source from a JSData resource 29 | * @class JSDataDataSource 30 | * @param {JSDataResource} resource 31 | * The JSData resource representing the entity set. 32 | * @param {Object} options 33 | * The query options. You can use it add JSData life cycle hooks. 34 | */ 35 | export class JSDataDataSource extends DataSource { 36 | constructor(resource, options = {}) { 37 | super(resource.idAttribute); 38 | this._resource = resource; 39 | this._options = options; 40 | } 41 | 42 | query(params) { 43 | const options = _.defaults({}, params.options, this._options); 44 | 45 | return this._resource 46 | .findAll(translateParams(this, _.omit(params, 'options')), options) 47 | .then(data => { 48 | if (_.isArray(data)) { 49 | return ({ 50 | items: data.slice(), 51 | totalCount: data.totalCount || 0, 52 | }); 53 | } else { 54 | throw data; // propagate error out 55 | } 56 | }); 57 | } 58 | 59 | get resource() { 60 | return this._resource; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /js/vnext/data-source/memory.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import { DataSource } from './base.js'; 3 | 4 | const truthy = _.constant(true); 5 | 6 | /** 7 | * Data source from an in-memory array. 8 | * @class MemoryDataSource 9 | * @param {Object[]} data 10 | * The in-memory array for the data set. 11 | * @param {string} primaryKey 12 | * The primary key of the data set. 13 | */ 14 | export class MemoryDataSource extends DataSource { 15 | constructor(data, primaryKey) { 16 | super(primaryKey); 17 | this.data = data; 18 | 19 | // This caches the query result without skip and take 20 | this._cache = { 21 | seed: this.data, 22 | query: null, 23 | data: this.data, 24 | }; 25 | } 26 | 27 | /** 28 | * The cached items respecting the sorting and filtering, 29 | * ignoring the skip and take. 30 | * @type {Object[]} 31 | */ 32 | get cachedItems() { 33 | return this._cache.data; 34 | } 35 | 36 | query(params) { 37 | const { 38 | skip = 0, 39 | take = this.data.length - skip, 40 | filter = truthy, 41 | orderby = [], 42 | } = params || {}; 43 | 44 | if (this._cache.seed !== this.data || !_.isEqual(this._cache.query, { filter, orderby })) { 45 | const { key, direction } = _.first(orderby) || {}; 46 | let sortIteratee = null; 47 | 48 | if (_.isFunction(key)) { 49 | sortIteratee = key; 50 | } else if (_.isString(key)) { 51 | const segs = key.split(/[\.\/]/); 52 | sortIteratee = item => _.reduce(segs, (memo, seg) => _.result(memo, seg), item); 53 | } 54 | 55 | let data = _.filter(this.data, filter); 56 | 57 | if (sortIteratee) { 58 | data = _.sortBy(data, sortIteratee); 59 | 60 | if (direction < 0) { 61 | data.reverse(); 62 | } 63 | } 64 | 65 | this._cache = { 66 | seed: this.data, 67 | query: { filter, orderby }, 68 | data, 69 | }; 70 | } 71 | 72 | return { 73 | totalCount: this._cache.data.length, 74 | items: this._cache.data.slice(skip, skip + take), 75 | }; 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /js/vnext/data-source/odata.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import $ from 'jquery'; 3 | import Promise from 'bluebird'; 4 | import { DataSource } from './base.js'; 5 | 6 | function translateOrderBy(dataSource, orderByParams) { 7 | return _.chain(dataSource.normalizeOrderBy(orderByParams)) 8 | .map(([key, direction]) => `${key} ${direction > 0 ? 'asc' : 'desc'}`) 9 | .join(',') 10 | .value(); 11 | } 12 | 13 | function translateParams(dataSource, params) { 14 | return _.chain(params) 15 | .omit('skip', 'take', 'filter', 'orderby') 16 | .extend(_.pick({ 17 | $skip: params.skip, 18 | $top: params.take, 19 | $filter: params.filter, 20 | $orderby: translateOrderBy(dataSource, params.orderby), 21 | $count: true, 22 | }, Boolean)) 23 | .value(); 24 | } 25 | 26 | /** 27 | * Data source from an OData service. 28 | * @class ODataDataSource 29 | * @param {string} url 30 | * The root URL of the OData entity set. 31 | * @param {string} primaryKey 32 | * The primary key of the entity set. 33 | */ 34 | export class ODataDataSource extends DataSource { 35 | constructor(url, primaryKey) { 36 | super(primaryKey); 37 | this._url = url; 38 | } 39 | 40 | query(params) { 41 | return new Promise((resolve, reject) => { 42 | $.getJSON(this._url, translateParams(this, params)) 43 | .success(resolve) 44 | .fail((jqXHR, textStatus, errorThrown) => { 45 | reject(new Error(errorThrown)); 46 | }); 47 | }).then(data => ({ 48 | items: data.value || [], 49 | totalCount: data['@odata.count'] || 0, 50 | })); 51 | } 52 | 53 | get url() { 54 | return this._url; 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /js/vnext/factory/data-source-plugin.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import { 3 | DataSource, 4 | ODataDataSource, 5 | JSDataDataSource, 6 | MemoryDataSource, 7 | } from '../data-source'; 8 | 9 | /** 10 | * @typedef DataSourceConfig 11 | * @type {DataSource|BuiltinDataSourceConfig} 12 | */ 13 | 14 | /** 15 | * @typedef BuiltinDataSourceConfig 16 | * @type {Object} 17 | * @property {string} type 18 | * The builtin data source type, available values are 'odata', 'js-data' 19 | * and 'memory'. 20 | * @property {string?} primaryKey 21 | * The primaryKey for the data source. Required by 'odata' and 'memory' 22 | * data sources. 23 | * @property {JSDataResource?} entity 24 | * The JSDataResource representing the entity set. Required by 'js-data' 25 | * data source. 26 | * @property {string?} url 27 | * The URL for the entity set. Required by 'odata' data source. 28 | * @property {Object[]?} data 29 | * The in-memory data set. Required by 'memory' data source. 30 | * 31 | */ 32 | 33 | export default definePlugin => definePlugin('dataSource', ['config'], config => { 34 | const dataSource = _.result(config, 'dataSource', {}); 35 | 36 | if (dataSource instanceof DataSource) { 37 | return dataSource; 38 | } 39 | 40 | const { type, primaryKey } = dataSource; 41 | 42 | if (type === 'memory') { 43 | return new MemoryDataSource(dataSource.data, primaryKey); 44 | } 45 | 46 | if (type === 'odata') { 47 | return new ODataDataSource(dataSource.url, primaryKey); 48 | } 49 | 50 | if (_.contains(['jsdata', 'js-data'], type)) { 51 | return new JSDataDataSource(dataSource.entity, dataSource.options); 52 | } 53 | 54 | throw new Error('Unsupported data source type'); 55 | }); 56 | 57 | -------------------------------------------------------------------------------- /js/vnext/factory/grid-view-plugin.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import { GridView } from '../grid-view.js'; 3 | 4 | const CONSTRUCTOR_OPTIONS = ['el', 'scrolling', 'tableClasses', 'tableAttributes', 'layout']; 5 | const NONE_PROJECTION_OPTIONS = CONSTRUCTOR_OPTIONS.concat(['plugins']); 6 | 7 | /** 8 | * @typedef GridViewConfig 9 | * @type {Object} 10 | * @property {string} name 11 | * Name of the grid 12 | * @property {string|HTMLElement|jQuery} el 13 | * The root element of the grid view. 14 | * @property {ScrollingConfig} scrolling 15 | * The scrolling behavior configuratoin. 16 | * @property {strings[]} tableClasses 17 | * An array of classes to be applied to `TABLE` elements. 18 | * @property {object} tableAttributes 19 | * An object of attributes to be applied to `TABLE` elements. 20 | * @property {Object.} plugins 21 | * The plugin configurations. The keys are the plugin names, and the values 22 | * are the configuation objects. 23 | * @property {RowsConfig} rows 24 | * The structure of the head/body/foot rows. 25 | * @property {ColumnConfig[]} columns 26 | * Array of the columns configurations. 27 | * @property {DataSourceConfig} dataSource 28 | * The data source configurations. Tells the grid how to fetch data. 29 | * @property {QueryConfig} query 30 | * The query parameters for data fetch. 31 | * @property {SelectionConfig} selection 32 | * Config the behavior for row selection. 33 | * @property {SortableHeaderConfig} sortableHeader 34 | * Config the state and visual of the click-to-sort column headers. 35 | * @property {external:BackboneViewEventHash} events 36 | * Customized event hash in form of `Backbone.View#events`. 37 | * It can be set to handle 38 | * * The DOM events inside the `GridView`. 39 | * * The `GridView` events, e.g. {@link GridView#willUpdate}, 40 | * {@link GridView#didUpdate}. 41 | */ 42 | export default definePlugin => definePlugin('gridView', [ 43 | 'config', 44 | 'dataSource', 45 | ], (config, dataSource) => { 46 | const options = _.chain(config) 47 | .pick(CONSTRUCTOR_OPTIONS) 48 | .extend({ dataSource }) 49 | .value(); 50 | const gridView = new GridView(options); 51 | 52 | gridView.set(_.omit(config, NONE_PROJECTION_OPTIONS)); 53 | 54 | return gridView; 55 | }); 56 | 57 | -------------------------------------------------------------------------------- /js/vnext/layout/column-group.jade: -------------------------------------------------------------------------------- 1 | each col in cols || [] 2 | - var columnStyle = col.width ? 'width: ' + col.width + ';' : null; 3 | - var attr = escapeAttr(col.attributes); 4 | col&attributes(attr)(class=col.classes, style=columnStyle) 5 | -------------------------------------------------------------------------------- /js/vnext/layout/const.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import { escapeAttr } from './escape'; 3 | 4 | import tableStickyTemplate from './table-sticky.jade'; 5 | import flexStickyTemplate from './flex-sticky.jade'; 6 | import tableFixedTemplate from './table-fixed.jade'; 7 | import flexFixedTemplate from './flex-fixed.jade'; 8 | import tableStaticTemplate from './table-static.jade'; 9 | import flexStaticTemplate from './flex-static.jade'; 10 | 11 | import tableRowTemplate from './row.jade'; 12 | import flexRowTemplate from './flex-row.jade'; 13 | 14 | import tableHeaderFooterTemplate from './header-footer.jade'; 15 | import flexHeaderFooterTemplate from './flex-header-footer.jade'; 16 | 17 | export const LAYOUT = { 18 | table: 'table', 19 | flex: 'flex', 20 | }; 21 | 22 | // sticky/static/fixed header type 23 | export const stickyTemplate = { 24 | [LAYOUT.table]: tableStickyTemplate, 25 | [LAYOUT.flex]: flexStickyTemplate, 26 | }; 27 | 28 | export const fixedTemplate = { 29 | [LAYOUT.table]: tableFixedTemplate, 30 | [LAYOUT.flex]: flexFixedTemplate, 31 | }; 32 | export const staticTemplate = { 33 | [LAYOUT.table]: tableStaticTemplate, 34 | [LAYOUT.flex]: flexStaticTemplate, 35 | }; 36 | 37 | // header footer template 38 | export const headerFooterTemplate = { 39 | [LAYOUT.table]: tableHeaderFooterTemplate, 40 | [LAYOUT.flex]: flexHeaderFooterTemplate, 41 | }; 42 | 43 | // row template 44 | export const rowTemplateWithEscape = { 45 | [LAYOUT.table]: row => tableRowTemplate(_.defaults({ escapeAttr }, row)), 46 | [LAYOUT.flex]: row => flexRowTemplate(_.defaults({ escapeAttr }, row)), 47 | }; 48 | 49 | // selector 50 | export const colgroupSelector = { 51 | [LAYOUT.table]: 'colgroup.column-group', 52 | [LAYOUT.flex]: '.colgroup.column-group', 53 | }; 54 | export const headerSelector = { 55 | [LAYOUT.table]: 'thead.header', 56 | [LAYOUT.flex]: '.thead.header', 57 | }; 58 | export const footerSelector = { 59 | [LAYOUT.table]: 'tfoot.footer', 60 | [LAYOUT.flex]: '.tfoot.footer', 61 | }; 62 | export const stickyHeaderFillerTableSelector = { 63 | [LAYOUT.table]: '.sticky-header-filler + table', 64 | [LAYOUT.flex]: '.sticky-header-filler + .table', 65 | }; 66 | export const viewportTableSelector = { 67 | [LAYOUT.table]: '.viewport > table', 68 | [LAYOUT.flex]: '.viewport > .table', 69 | }; 70 | -------------------------------------------------------------------------------- /js/vnext/layout/editable.jade: -------------------------------------------------------------------------------- 1 | .grid-edit-icon(title='Edit', class=classes) 2 | !=$html 3 | =text -------------------------------------------------------------------------------- /js/vnext/layout/escape.js: -------------------------------------------------------------------------------- 1 | const pugMatch = /["&<>]/; 2 | 3 | function pugEscape(_html) { 4 | const html = String(_html); 5 | const regexResult = pugMatch.exec(html); 6 | if (!regexResult) { 7 | return _html; 8 | } 9 | 10 | let result = ''; 11 | let i, lastIndex, escape; 12 | for (i = regexResult.index, lastIndex = 0; i < html.length; i++) { 13 | switch (html.charCodeAt(i)) { 14 | case 34: escape = '"'; break; 15 | case 38: escape = '&'; break; 16 | case 60: escape = '<'; break; 17 | case 62: escape = '>'; break; 18 | default: continue; 19 | } 20 | if (lastIndex !== i) { 21 | result += html.substring(lastIndex, i); 22 | } 23 | lastIndex = i + 1; 24 | result += escape; 25 | } 26 | if (lastIndex !== i) { 27 | return result + html.substring(lastIndex, i); 28 | } 29 | 30 | return result; 31 | } 32 | 33 | export function escapeAttr(attr) { 34 | if (Array.isArray(attr)) { 35 | return attr.map(function (val) { 36 | return escapeAttr(val); 37 | }); 38 | } else if (typeof attr === 'object') { 39 | return Object.keys(attr).reduce(function (memo, key) { 40 | memo[key] = escapeAttr(attr[key]); 41 | return memo; 42 | }, {}); 43 | } else if (typeof attr === 'string') { 44 | return pugEscape(attr); 45 | } 46 | return attr; 47 | } 48 | -------------------------------------------------------------------------------- /js/vnext/layout/flex-fixed.jade: -------------------------------------------------------------------------------- 1 | - 2 | var tableStyle = { 3 | 'border-collapse': 'collapse', 4 | 'margin': 0, 5 | 'table-layout': 'fixed', 6 | }; 7 | 8 | .table-container.flex-layout(role='grid')&attributes(dataTableAttributes) 9 | .fixed-header 10 | div.table.fixed-header(style=tableStyle, class=classes)&attributes(headerAttributes) 11 | div.thead.header 12 | .viewport 13 | div.table.data-table(style=tableStyle, class=classes) 14 | div.tbody(role='rowgroup') 15 | div.tr.top-filler(role='presentation') 16 | div.tr.bottom-filler(role='presentation') 17 | div.tfoot.footer 18 | -------------------------------------------------------------------------------- /js/vnext/layout/flex-head-cell.jade: -------------------------------------------------------------------------------- 1 | include ./flex-mixins.jade 2 | 3 | +mixinHeadCell(cell, escapeAttr) 4 | -------------------------------------------------------------------------------- /js/vnext/layout/flex-header-footer.jade: -------------------------------------------------------------------------------- 1 | include ./flex-mixins.jade 2 | 3 | +mixinRow(row, group, escapeAttr) 4 | -------------------------------------------------------------------------------- /js/vnext/layout/flex-layout.less: -------------------------------------------------------------------------------- 1 | .flex-layout { 2 | .flex-row, 3 | .flex-column-group, 4 | .flex-column-group-container, 5 | .nested-column-container, 6 | .nested-column-placeholder { 7 | display: flex; 8 | } 9 | 10 | .flex-column-group, 11 | .flex-column-group-container { 12 | flex-grow: 1; 13 | } 14 | 15 | .nested-column-container > .flex-cell { 16 | width: 100%; 17 | } 18 | 19 | .nested-column-container { 20 | flex-direction: column; 21 | } 22 | 23 | .flex-cell { 24 | display: flex; 25 | flex-grow: 1; 26 | flex-shrink: 0; 27 | align-items: center; 28 | padding: 6px 8px; 29 | border-right: 1px solid #EDEBE9; 30 | border-bottom: 1px solid #EDEBE9; 31 | } 32 | } -------------------------------------------------------------------------------- /js/vnext/layout/flex-mixins.jade: -------------------------------------------------------------------------------- 1 | mixin cellValue(cell) 2 | if (cell.html) 3 | != cell.html 4 | else 5 | = cell.value 6 | 7 | mixin mixinHeadCell(cell, escapeAttr) 8 | - var attr = escapeAttr(cell.attributes) 9 | if attr.colspan > 1 10 | .nested-column-container(style=`flex-grow: ${attr.colspan};`) 11 | div.th.flex-cell&attributes(attr)(class=cell.classes, role='columnheader') 12 | +cellValue(cell) 13 | if (cell.hasChild) 14 | .nested-column-placeholder 15 | else 16 | div.th.flex-cell&attributes(attr)(class=cell.classes, role='columnheader') 17 | +cellValue(cell) 18 | if (cell.hasChild) 19 | .nested-column-placeholder 20 | 21 | mixin mixinCell(cell, escapeAttr) 22 | - var attr = escapeAttr(cell.attributes) 23 | div.td.flex-cell&attributes(attr)(class=cell.classes, role='gridcell') 24 | +cellValue(cell) 25 | 26 | mixin mixinSection(section, cell, escapeAttr) 27 | case section 28 | when 'head' 29 | +mixinHeadCell(cell, escapeAttr) 30 | when 'body' 31 | when 'foot' 32 | +mixinCell(cell, escapeAttr) 33 | 34 | mixin mixinRow(row, section, escapeAttr) 35 | - var attr = escapeAttr(row.attributes) 36 | 37 | div.tr.flex-row&attributes(attr)(class=row.classes, role='row') 38 | if row.html || row.view 39 | each cell in row.cells 40 | +mixinSection(section, cell, escapeAttr) 41 | else 42 | - var groups = [], cellByGroup = (row.cells || []).reduce((memo, cell) => { 43 | - var group = cell.group; 44 | - !memo[group] && (memo[group] = [], groups.push(group)); 45 | - memo[group].push(cell); 46 | - return memo; 47 | - }, {}); 48 | 49 | each group in groups 50 | div(class=`${group}-group-container flex-column-group-container`) 51 | div(class=`${group}-group flex-column-group`) 52 | each cell in cellByGroup[group] 53 | +mixinSection(section, cell, escapeAttr) 54 | 55 | 56 | mixin mixinRows(rows, group, escapeAttr) 57 | each row in rows || [] 58 | +mixinRow(row, group, escapeAttr) -------------------------------------------------------------------------------- /js/vnext/layout/flex-row.jade: -------------------------------------------------------------------------------- 1 | include ./flex-mixins.jade 2 | 3 | +mixinRow(row, 'body', escapeAttr) 4 | -------------------------------------------------------------------------------- /js/vnext/layout/flex-static.jade: -------------------------------------------------------------------------------- 1 | - 2 | var tableStyle = { 3 | 'border-collapse': 'collapse', 4 | 'margin': 0, 5 | }; 6 | 7 | .table-container.flex-layout(role='grid')&attributes(dataTableAttributes) 8 | div.table.data-table(style=tableStyle, class=classes) 9 | div.thead.header 10 | div.tbody(role='rowgroup') 11 | div.tr.top-filler(role='presentation') 12 | div.tr.bottom-filler(role='presentation') 13 | div.tfoot.footer 14 | -------------------------------------------------------------------------------- /js/vnext/layout/flex-sticky.jade: -------------------------------------------------------------------------------- 1 | - 2 | var tableStyle = { 3 | 'border-collapse': 'collapse', 4 | 'margin': 0, 5 | 'table-layout': 'fixed', 6 | }; 7 | 8 | .table-container.flex-layout(role='grid')&attributes(dataTableAttributes) 9 | div.table.sticky-header(style=tableStyle, class=classes)&attributes(headerAttributes) 10 | div.thead.header 11 | .sticky-header-filler 12 | div.table.data-table(style=tableStyle, class=classes) 13 | div.tbody(role='rowgroup') 14 | div.tr.top-filler(role='presentation') 15 | div.tr.bottom-filler(role='presentation') 16 | div.tfoot.footer 17 | 18 | -------------------------------------------------------------------------------- /js/vnext/layout/header-footer.jade: -------------------------------------------------------------------------------- 1 | include ./table-mixins.jade 2 | 3 | +mixinRow(row, group, escapeAttr) 4 | -------------------------------------------------------------------------------- /js/vnext/layout/index.js: -------------------------------------------------------------------------------- 1 | export { TableView } from './table-view'; 2 | -------------------------------------------------------------------------------- /js/vnext/layout/row.jade: -------------------------------------------------------------------------------- 1 | include ./table-mixins.jade 2 | 3 | +mixinRow(row, 'body', escapeAttr) 4 | -------------------------------------------------------------------------------- /js/vnext/layout/table-fixed.jade: -------------------------------------------------------------------------------- 1 | - 2 | var tableStyle = { 3 | 'border-collapse': 'collapse', 4 | 'margin': 0, 5 | 'table-layout': 'fixed', 6 | }; 7 | 8 | .table-container 9 | .fixed-header 10 | table.fixed-header(style=tableStyle, class=classes, role='grid')&attributes(headerAttributes) 11 | colgroup.column-group 12 | thead.header 13 | .viewport 14 | table.data-table(style=tableStyle, class=classes, role='grid')&attributes(dataTableAttributes) 15 | colgroup.column-group 16 | tbody 17 | tr.top-filler(role='presentation') 18 | tr.bottom-filler(role='presentation') 19 | tfoot.footer 20 | -------------------------------------------------------------------------------- /js/vnext/layout/table-mixins.jade: -------------------------------------------------------------------------------- 1 | mixin mixinHeadCell(cell, escapeAttr) 2 | - var attr = escapeAttr(cell.attributes) 3 | th&attributes(attr)(class=cell.classes) 4 | if (cell.html) 5 | != cell.html 6 | else 7 | = cell.value 8 | 9 | mixin mixinCell(cell, escapeAttr) 10 | - var attr = escapeAttr(cell.attributes) 11 | td&attributes(attr)(class=cell.classes) 12 | if (cell.html) 13 | != cell.html 14 | else 15 | = cell.value 16 | 17 | mixin mixinRow(row, group, escapeAttr) 18 | - var attr = escapeAttr(row.attributes) 19 | tr&attributes(attr)(class=row.classes) 20 | each cell in row.cells || [] 21 | case group 22 | when 'head' 23 | +mixinHeadCell(cell, escapeAttr) 24 | when 'body' 25 | when 'foot' 26 | +mixinCell(cell, escapeAttr) 27 | 28 | mixin mixinRows(rows, group, escapeAttr) 29 | each row in rows || [] 30 | +mixinRow(row, group, escapeAttr) -------------------------------------------------------------------------------- /js/vnext/layout/table-static.jade: -------------------------------------------------------------------------------- 1 | - 2 | var tableStyle = { 3 | 'border-collapse': 'collapse', 4 | 'margin': 0, 5 | }; 6 | 7 | .table-container 8 | table.data-table(style=tableStyle, class=classes, role='grid')&attributes(dataTableAttributes) 9 | colgroup.column-group 10 | thead.header 11 | tbody 12 | tr.top-filler(role='presentation') 13 | tr.bottom-filler(role='presentation') 14 | tfoot.footer 15 | -------------------------------------------------------------------------------- /js/vnext/layout/table-sticky.jade: -------------------------------------------------------------------------------- 1 | - 2 | var tableStyle = { 3 | 'border-collapse': 'collapse', 4 | 'margin': 0, 5 | 'table-layout': 'fixed', 6 | }; 7 | 8 | .table-container 9 | table.sticky-header(style=tableStyle, class=classes, role='grid')&attributes(headerAttributes) 10 | colgroup.column-group 11 | thead.header 12 | .sticky-header-filler 13 | table.data-table(style=tableStyle, class=classes, role='grid')&attributes(dataTableAttributes) 14 | colgroup.column-group 15 | tbody 16 | tr.top-filler(role='presentation') 17 | tr.bottom-filler(role='presentation') 18 | tfoot.footer 19 | 20 | -------------------------------------------------------------------------------- /js/vnext/projection/buffer.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | 3 | function updateItemState(gridView, item, state) { 4 | const buffer = gridView.get('buffer'); 5 | const changed = {}; 6 | const key = _.result(item, gridView.primaryKey); 7 | 8 | changed[key] = { 9 | item, 10 | state, 11 | }; 12 | _.defaults(changed, buffer.changed); 13 | 14 | gridView.set({ 15 | buffer: _.defaults({ changed }, buffer), 16 | }); 17 | } 18 | 19 | export const buffer = { 20 | name: 'buffer', 21 | handler(state, buffer) { 22 | const { primaryKey, uniqueId, update } = state; 23 | 24 | if (buffer.uniqueId !== uniqueId) { 25 | buffer.uniqueId = uniqueId; 26 | buffer.changed = {}; 27 | } 28 | 29 | const items = { 30 | length: state.items.length, 31 | slice: (begin = 0, end = state.items.length) => { 32 | return state.items.slice(begin, end).map(item => { 33 | const key = item[primaryKey]; 34 | 35 | if (_.has(buffer.changed, key)) { 36 | return buffer.changed[key].item; 37 | } 38 | return item; 39 | }); 40 | }, 41 | }; 42 | 43 | const onCommit = item => { 44 | if (item) { 45 | updateItemState(this, item, 'committed'); 46 | } 47 | }; 48 | 49 | const onEdit = item => { 50 | if (item) { 51 | updateItemState(this, item, 'changed'); 52 | if (_.isFunction(update)) { 53 | update(item).then(onCommit); 54 | } 55 | } 56 | }; 57 | 58 | const events = _.defaults({ didEdit: onEdit }, state.events); 59 | 60 | return _.defaults({ items, events }, state); 61 | }, 62 | defaults: {}, 63 | }; 64 | 65 | -------------------------------------------------------------------------------- /js/vnext/projection/default-cell.jade: -------------------------------------------------------------------------------- 1 | span 2 | =value -------------------------------------------------------------------------------- /js/vnext/projection/events.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | 3 | function sequence(...args) { 4 | const callbacks = _.filter(args, _.isFunction); 5 | return function (...argsInner) { 6 | _.each(callbacks, cb => cb.apply(this, argsInner)); 7 | }; 8 | } 9 | 10 | /** 11 | * @external BackboneViewEventHash 12 | * @see {@link http://backbonejs.org/#View-events} 13 | */ 14 | 15 | /** 16 | * Merge the customized events with the projection injected events. 17 | * @param {ContentChainState} state 18 | * The input state. 19 | * @param {external:BackboneViewEventHash} eventsOptions 20 | * The customized events in form of `Backbone.View#events`. 21 | */ 22 | function eventsProjectionHandler(state, eventsOptions) { 23 | const pairs = _.pairs(state.events).concat(_.pairs(eventsOptions)); 24 | const events = _.reduce(pairs, (memo, [key, handler]) => { 25 | return _.extend(memo, { 26 | [key]: sequence(memo[key], handler) 27 | }); 28 | }, {}); 29 | 30 | return _.defaults({ events }, state); 31 | } 32 | 33 | export const events = { 34 | name: 'events', 35 | handler: eventsProjectionHandler, 36 | defaults: {}, 37 | }; 38 | 39 | -------------------------------------------------------------------------------- /js/vnext/projection/flex-columns.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import { getDefaultColumns, extendColumns, extendWidthAttr, extendDataColumnAttr } from './common.js'; 3 | 4 | function columnsProjectionHandler(state, columns) { 5 | const defaultColumns = getDefaultColumns(state, columns); 6 | const extendedColumnsWithDataColumn = extendColumns(defaultColumns, extendDataColumnAttr); 7 | const extendedColumnsWithDataColumnWithWidth = extendColumns(extendedColumnsWithDataColumn, extendWidthAttr); 8 | 9 | return _.defaults({ 10 | columns: extendedColumnsWithDataColumnWithWidth, 11 | }, state); 12 | } 13 | 14 | // only pipe this projection when the layout is 'flex' 15 | export const flexColumns = { 16 | name: 'columns', 17 | handler: columnsProjectionHandler, 18 | defaults: null, 19 | }; 20 | -------------------------------------------------------------------------------- /js/vnext/projection/index.js: -------------------------------------------------------------------------------- 1 | export { query } from './query.js'; 2 | export { buffer } from './buffer.js'; 3 | export { itemIndex } from './item-index.js'; 4 | export { selection, setSelectAll, setSelectRow } from './selection.js'; 5 | export { rangeSelection } from './range-selection.js'; 6 | export { columns } from './columns.js'; 7 | export { flexColumns } from './flex-columns.js'; 8 | export { rows } from './rows.js'; 9 | export { columnGroup } from './column-group.js'; 10 | export { cells } from './cells.js'; 11 | export { sortableHeader } from './sortable-header.js'; 12 | export { events } from './events.js'; 13 | -------------------------------------------------------------------------------- /js/vnext/projection/item-index.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | 3 | export const itemIndex = { 4 | name: 'itemIndex', 5 | handler(state) { 6 | const { items, primaryKey } = state; 7 | const itemIndex = {}; 8 | 9 | _.each(items.slice(), item => { 10 | if (!_.has(item, primaryKey)) { 11 | item[primaryKey] = _.uniqueId('grid-item-'); 12 | } 13 | itemIndex[item[primaryKey]] = item; 14 | }); 15 | 16 | return _.defaults({ itemIndex }, state); 17 | }, 18 | defaults: {}, 19 | }; 20 | 21 | -------------------------------------------------------------------------------- /js/vnext/projection/multiple-selection-resolver.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import { SelectionResolver } from './selection-resolver.js'; 3 | 4 | export class MultipleSelectionResolver extends SelectionResolver { 5 | 6 | selectRow(key) { 7 | return this.patch(({ selected }) => ({ 8 | selected: _.union(selected, [key.toString()]), 9 | })); 10 | } 11 | 12 | deselectRow(key) { 13 | return this.patch(({ selected }) => ({ 14 | selected: _.without(selected, key.toString()), 15 | })); 16 | } 17 | 18 | // Select all selectalbe items 19 | selectAll() { 20 | return this.patch(({ selectable, selected }) => { 21 | const patch = {}; 22 | 23 | patch.selected = _.chain(this.gridView.items.slice()) 24 | .filter(selectable) 25 | .map(_.property(this.gridView.primaryKey)) 26 | .map(String) 27 | .union(selected) 28 | .value(); 29 | 30 | return patch; 31 | }); 32 | } 33 | 34 | // Deselect all selectable items 35 | deselectAll() { 36 | return this.patch(({ selectable, selected }) => { 37 | const patch = {}; 38 | 39 | patch.selected = _.filter(selected, key => { 40 | const item = this.gridView.itemWithKey(key); 41 | 42 | return !(item && selectable(item)); 43 | }); 44 | 45 | return patch; 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /js/vnext/projection/query.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import Promise from 'bluebird'; 3 | 4 | /** 5 | * The data source configurations. 6 | * @typedef QueryConfig 7 | * @type {Object} 8 | * @property {OrderByConfig} orderby 9 | * The sorting parameter. 10 | * @property {Object} filter 11 | * The filter object in MangoDB format. 12 | * @property {number} skip 13 | * The number of item to skip from the query result. 14 | * @property {number} take 15 | * The number of item to take from the query result. 16 | */ 17 | 18 | /** 19 | * Fetching data from the data source 20 | * @param {DataChainState} state 21 | * @param {QueryConfig} params 22 | * The data source configurations. 23 | * @return {DataChainState} 24 | */ 25 | function queryProjectionHandler(state, params) { 26 | const primaryKey = this.primaryKey; 27 | 28 | /** 29 | * The `GridView` will reload data from the data source. 30 | * @event GridView#willReload 31 | */ 32 | this.trigger('willReload'); 33 | 34 | return Promise.resolve(this.query(params)).catch(error => ({ 35 | totalCount: 0, 36 | items: [], 37 | error: error || new Error('Failed to load grid data'), 38 | })).then(({ totalCount, items, error }) => { 39 | /** 40 | * The `GridView` did reload data from the data source. 41 | * This event take 2 parameters 42 | * 1. succeeded (Boolean), true if the data fetch succeeded 43 | * 2. data|rejection (Object) 44 | * * If the data fetch succeeded, pass the fetched data in form of 45 | * `{ totalCount, items }`. Where `totalCount` is the server side total 46 | * item count, and the items is the array of data items with current 47 | * query condition 48 | * * If the data fetch failed, pass the rejection object, which is 49 | * usually an `Error` object from the data source 50 | * @event GridView#didReload 51 | */ 52 | if (error) { 53 | this.trigger('didReload', false, error); 54 | } else { 55 | this.trigger('didReload', true, { totalCount, items }); 56 | } 57 | 58 | return { 59 | uniqueId: _.uniqueId('grid-data-'), 60 | items, 61 | primaryKey, 62 | totalCount, 63 | }; 64 | }); 65 | } 66 | 67 | export const query = { 68 | name: 'query', 69 | handler: queryProjectionHandler, 70 | defaults: {}, 71 | }; 72 | -------------------------------------------------------------------------------- /js/vnext/projection/range-selection.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | 3 | function changeSelectRow(e) { 4 | const key = this.keyOfElement(e.target); 5 | const selection = this.get('selection'); 6 | const rangeSelection = this.get('rangeSelection'); 7 | 8 | if (!e.shiftKey || selection.disableShiftKey) { 9 | rangeSelection.preSelect = key; 10 | return; 11 | } 12 | 13 | const selected = selection.selected; 14 | const selectable = selection.selectable; 15 | const preSelect = rangeSelection.preSelect; 16 | const preShiftSelect = rangeSelection.preShiftSelect; 17 | const allKey = _.chain(this.items.slice()) 18 | .filter(selectable) 19 | .map(_.property(this.primaryKey)) 20 | .map(String) 21 | .value(); 22 | const preIndex = allKey.indexOf(preSelect); 23 | const curIndex = allKey.indexOf(key); 24 | const selectObj = _.indexBy(selected); 25 | 26 | if (preShiftSelect) { 27 | const preShiftIndex = allKey.indexOf(preShiftSelect); 28 | 29 | _.each(allKey.slice( 30 | Math.min(preIndex, preShiftIndex), 31 | Math.max(preIndex, preShiftIndex) + 1 32 | ), key => { 33 | delete selectObj[key]; 34 | }); 35 | } 36 | 37 | _.each(allKey.slice( 38 | Math.min(preIndex, curIndex), 39 | Math.max(preIndex, curIndex) + 1 40 | ), key => { 41 | selectObj[key] = key; 42 | }); 43 | 44 | rangeSelection.preShiftSelect = key; 45 | this.set({ 46 | selection: _.defaults({ selected: _.keys(selectObj) }, selection), 47 | }); 48 | } 49 | 50 | function changeSelectAll() { 51 | const rangeSelection = this.get('rangeSelection'); 52 | 53 | rangeSelection.preSelect = null; 54 | rangeSelection.preShiftSelect = null; 55 | } 56 | 57 | function selectionProjectionHandler(state, config, model) { 58 | if (model.selection.single) { 59 | return state; 60 | } 61 | 62 | const events = _.defaults({ 63 | 'click input.select-all': changeSelectAll, 64 | 'click input.select-row': changeSelectRow, 65 | }, state.events); 66 | 67 | return _.defaults({ events }, state); 68 | } 69 | 70 | export const rangeSelection = { 71 | name: 'rangeSelection', 72 | handler: selectionProjectionHandler, 73 | defaults: { 74 | preSelect: null, 75 | preShiftSelect: null, 76 | }, 77 | }; 78 | -------------------------------------------------------------------------------- /js/vnext/projection/selection-body.jade: -------------------------------------------------------------------------------- 1 | if value.selectable 2 | if value.single 3 | input.select-row(type='radio', checked=value.checked, aria-label=value.ariaLabel, aria-labelledby=value.labelbyId) 4 | else 5 | input.select-row(type='checkbox', checked=value.checked, aria-label=value.ariaLabel, aria-labelledby=value.labelbyId) 6 | else 7 | span.unselectable 8 | -------------------------------------------------------------------------------- /js/vnext/projection/selection-head.jade: -------------------------------------------------------------------------------- 1 | if single 2 | span 3 | else 4 | input.select-all(type='checkbox' checked=checked aria-label=checkAllLabel class=partSelected ? 'part-selected' : undefined) 5 | -------------------------------------------------------------------------------- /js/vnext/projection/selection-resolver.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | 3 | export class SelectionResolver { 4 | constructor(gridView) { 5 | this.gridView = gridView; 6 | } 7 | 8 | updateItems() { 9 | return this.patch({}); 10 | } 11 | 12 | selectRow() { 13 | throw new Error('Not Supported'); 14 | } 15 | 16 | deselectRow() { 17 | throw new Error('Not Supported'); 18 | } 19 | 20 | selectAll() { 21 | throw new Error('Not Supported'); 22 | } 23 | 24 | deselectAll() { 25 | throw new Error('Not Supported'); 26 | } 27 | 28 | patch(options) { 29 | const selectionCur = this.gridView.get('selection'); 30 | const selection = _.isFunction(options) ? options(selectionCur) : options; 31 | 32 | return _.defaults(selection, selectionCur); 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /js/vnext/projection/single-selection-resolver.js: -------------------------------------------------------------------------------- 1 | import { SelectionResolver } from './selection-resolver.js'; 2 | 3 | export class SingleSelectionResolver extends SelectionResolver { 4 | selectRow(key) { 5 | return this.patch({ selected: [key.toString()] }); 6 | } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /js/vnext/projection/sortable-header.jade: -------------------------------------------------------------------------------- 1 | if direction < 0 2 | span.sort-indicator.glyphicon.glyphicon-arrow-down 3 | else if direction > 0 4 | span.sort-indicator.glyphicon.glyphicon-arrow-up 5 | 6 | != html 7 | -------------------------------------------------------------------------------- /js/windowContainer.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'lib/underscore', 3 | 'lib/jquery', 4 | 'component/grid/containerBase', 5 | ], function (_, $, ContainerBase) { 6 | var WindowContainer = ContainerBase.extend({ 7 | constructor: function (options) { 8 | options = _.extend({}, options, { el: window }); 9 | ContainerBase.prototype.constructor.apply(this, [options].concat(_.rest(arguments))); 10 | }, 11 | 12 | offset: function (element) { 13 | return $(element).offset(); 14 | }, 15 | }); 16 | 17 | return WindowContainer; 18 | }); 19 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "opts": { 3 | "destination": "docs", 4 | "recurse": true 5 | }, 6 | "plugins": ["plugins/markdown"] 7 | } 8 | 9 | -------------------------------------------------------------------------------- /less/editable.less: -------------------------------------------------------------------------------- 1 | table.grid { 2 | td.grid-editable-cell { 3 | position: relative; 4 | 5 | .grid-edit-icon { 6 | display: none;; 7 | position: absolute; 8 | top: 2px; 9 | right: 2px; 10 | font-size: 10px; 11 | } 12 | } 13 | 14 | td.grid-editable-cell:hover { 15 | .grid-edit-icon { 16 | display: block; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /spec/integrated/$speclist.js: -------------------------------------------------------------------------------- 1 | // require all `test/**/*.js` except for `test/**/$*.js` 2 | // var testsContext = require.context('.', false, /[^\$][^\/]*\.js$/); 3 | var testsContext = require.context('.', true, /[^\$][^\/]*\-spec.js$/); 4 | testsContext.keys().forEach(testsContext); 5 | -------------------------------------------------------------------------------- /spec/integrated/conf/karma.conf.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var path = require('path'); 3 | 4 | function getWebpackConfig() { 5 | var webpackConfig = require('./webpack.config'); 6 | 7 | webpackConfig.module.preLoaders = [ 8 | { 9 | test: /\.js$/, 10 | include: path.resolve('./js/'), 11 | loader: 'isparta', 12 | }, 13 | ].concat(webpackConfig.module.preLoaders || []); 14 | return webpackConfig; 15 | } 16 | 17 | module.exports = function (config) { 18 | //files fild set in gulpfile.js in the root folder. 19 | config.set({ 20 | basePath: '../../../', 21 | frameworks: [ 22 | 'mocha', 23 | ], 24 | client: { 25 | mocha: { 26 | reporter: 'html', // change Karma's debug.html to the mocha web reporter 27 | }, 28 | }, 29 | reporters: ['mocha', 'coverage', 'junit'], 30 | webpack: getWebpackConfig(), 31 | coverageReporter: { 32 | dir: './spec/integrated/coverage', 33 | reporters: [ 34 | { type: 'html', subdir: 'report-html' }, 35 | { type: 'lcov', subdir: 'report-lcov' }, 36 | ], 37 | }, 38 | junitReporter: { 39 | outputDir: './spec/integrated/test-results/', 40 | }, 41 | browsers: [ 42 | 'ChromeNoSandbox', 43 | ], 44 | customLaunchers: { 45 | ChromeNoSandbox: { 46 | base: 'Chrome', 47 | flags: ['--no-sandbox'], 48 | }, 49 | }, 50 | 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /spec/integrated/conf/karma.debug.conf.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var path = require('path'); 3 | var webpackConfig = require('./webpack.config'); 4 | 5 | module.exports = function (config) { 6 | config.set({ 7 | basePath: '../../../', 8 | frameworks: [ 9 | 'mocha', 10 | ], 11 | reporters: [ 12 | 'mocha', 13 | ], 14 | client: { 15 | mocha: { 16 | reporter: 'html', // change Karma's debug.html to the mocha web reporter 17 | }, 18 | }, 19 | webpack: webpackConfig, 20 | browsers: [ 21 | 'ChromeNoSandbox', 22 | ], 23 | customLaunchers: { 24 | ChromeNoSandbox: { 25 | base: 'Chrome', 26 | flags: ['--no-sandbox'], 27 | }, 28 | }, 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /spec/integrated/conf/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var _ = require('underscore'); 3 | var webpackAlias = require('../../../webpack.alias'); 4 | _.extend(webpackAlias, { 5 | 'sinon': 'sinon/pkg/sinon.js', 6 | 'data' : path.resolve('./spec/integrated/data/'), 7 | 'driver' : path.resolve('./spec/integrated/driver/'), 8 | 'template' : path.resolve('./spec/integrated/template/'), 9 | 'util' : path.resolve('./spec/util/'), 10 | 'styles' : path.resolve('./spec/integrated/styles/'), 11 | }); 12 | 13 | module.exports = { 14 | output: { 15 | path: path.resolve('./dist'), 16 | filename: 'component-test.js', 17 | devtoolModuleFilenameTemplate: function (info) { 18 | if (path.isAbsolute(info.absoluteResourcePath)) { 19 | return 'webpack-src:///component-test/' + path.relative('.', info.absoluteResourcePath).replace(/\\/g, '/'); 20 | } 21 | return info.absoluteResourcePath; 22 | }, 23 | }, 24 | resolve: { 25 | alias: webpackAlias, 26 | }, 27 | module: { 28 | loaders: [ 29 | { 30 | test: /sinon\.js$/, 31 | loader: 'imports?define=>false,require=>false', 32 | }, 33 | // jade 34 | { test: /\.jade$/, loader: 'jade-loader' }, 35 | // jade-end 36 | // es2015 37 | { test: /\.js$/, exclude: /\bnode_modules\b/, loader: 'babel-loader' }, 38 | // es2015-end 39 | { test: /\.less$/, loader: 'style!css!less' }, 40 | { test: /\.css$/, loader: 'style!css!less' }, 41 | { test: /\.json$/, loader: 'json' }, 42 | ], 43 | 44 | }, 45 | babel: { presets: ['es2015'] }, 46 | devtool: 'inline-source-map', 47 | }; 48 | 49 | -------------------------------------------------------------------------------- /spec/integrated/data/js-data-expected.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "value": [ 4 | { 5 | "CustomerID": "ALFKI", 6 | "CompanyName": "Alfreds Futterkiste", 7 | "ContactName": "Maria Anders", 8 | "ContactTitle": "Sales Representative", 9 | "Address": "Obere Str. 57", 10 | "City": "Berlin", 11 | "Region": "", 12 | "PostalCode": "12209", 13 | "Country": "Germany", 14 | "Phone": "030-0074321", 15 | "Fax": "030-0076545" 16 | }, 17 | { 18 | "CustomerID": "BOTTM", 19 | "CompanyName": "Bottom-Dollar Markets", 20 | "ContactName": "Elizabeth Lincoln", 21 | "ContactTitle": "Accounting Manager", 22 | "Address": "23 Tsawassen Blvd.", 23 | "City": "Tsawassen", 24 | "Region": "BC", 25 | "PostalCode": "T2F 8M4", 26 | "Country": "Canada", 27 | "Phone": "(604) 555-4729", 28 | "Fax": "(604) 555-3745" 29 | }, 30 | { 31 | "CustomerID": "ERNSH", 32 | "CompanyName": "Ernst Handel", 33 | "ContactName": "Roland Mendel", 34 | "ContactTitle": "Sales Manager", 35 | "Address": "Kirchgasse 6", 36 | "City": "Graz", 37 | "Region": "", 38 | "PostalCode": "8010", 39 | "Country": "Austria", 40 | "Phone": "7675-3425", 41 | "Fax": "7675-3426" 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /spec/integrated/data/js-data-source.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import JSData from 'js-data'; 3 | import DSHttpAdapter from 'js-data-http'; 4 | 5 | JSData.DSUtils.Promise = require('bluebird'); 6 | let store = new JSData.DS(); 7 | 8 | store.registerAdapter('http', new DSHttpAdapter({ 9 | basePath: 'https://services.odata.org/V4/Northwind/Northwind.svc/', 10 | deserialize: (definition, response) => { 11 | return response.data.value; 12 | }, 13 | queryTransform: (definition, params) => { 14 | let query = {}; 15 | 16 | _.has(params, 'offset') && (query.$skip = params.offset); 17 | _.has(params, 'limit') && (query.$top = params.limit); 18 | _.has(params, 'orderBy') && (query.$orderby = params.orderBy.map((item) => { 19 | return item[0] + ' ' + item[1].toLowerCase(); 20 | }).join(',')); 21 | 22 | return query; 23 | }, 24 | }), { default: true }); 25 | 26 | module.exports = store.defineResource({ 27 | name: 'Customers', 28 | idAttribute: 'CustomerID', 29 | }); 30 | -------------------------------------------------------------------------------- /spec/integrated/driver/action.js: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird'; 2 | import protocol from './protocol'; 3 | 4 | function click(selector) { 5 | return protocol.element.call(this, selector) 6 | .then($el => { 7 | $el.click(); 8 | return null; 9 | }); 10 | } 11 | 12 | function setValue(selector, value) { 13 | return protocol.element.call(this, selector) 14 | .then(($el) => { 15 | $el.val(value).change(); 16 | return null; 17 | }); 18 | } 19 | 20 | export default { 21 | click, 22 | setValue, 23 | }; 24 | -------------------------------------------------------------------------------- /spec/integrated/driver/events.js: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird'; 2 | import _ from 'underscore'; 3 | 4 | function once(target, event) { 5 | return new Promise((resolve, reject) => { 6 | if (!_.isEmpty(target) && _.isFunction(target.once)) { 7 | target.once(event, (...args) => { 8 | resolve(...args); 9 | }) 10 | } else { 11 | reject(new Error('Target not exist or target no contains "event" function')); 12 | } 13 | }); 14 | } 15 | 16 | export default { 17 | once, 18 | }; 19 | -------------------------------------------------------------------------------- /spec/integrated/driver/index.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import protocol from './protocol'; 3 | import action from './action'; 4 | import utility from './utility'; 5 | import events from './events'; 6 | 7 | let exports = _.extend({}, protocol, action, utility, events); 8 | export default exports; -------------------------------------------------------------------------------- /spec/integrated/driver/protocol.js: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird'; 2 | import $ from 'jquery'; 3 | 4 | function element(el) { 5 | return new Promise((resolve, reject) => { 6 | let $el = el instanceof $ ? el : $(el); 7 | resolve($el); 8 | }); 9 | } 10 | 11 | export default { 12 | element, 13 | }; 14 | -------------------------------------------------------------------------------- /spec/integrated/driver/utility.js: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird'; 2 | import _ from 'underscore'; 3 | import protocol from './protocol'; 4 | 5 | function pause(ms) { 6 | return new Promise((resolve) => { 7 | setTimeout(() => { 8 | resolve(); 9 | }, ms); 10 | }); 11 | } 12 | 13 | function scroll(selector, xoffset, yoffset) { 14 | return new Promise((resolve, reject) => { 15 | if (!_.isString(selector)) { 16 | yoffset = xoffset; 17 | xoffset = selector; 18 | selector = undefined; 19 | } else if (arguments.length < 3) { 20 | xoffset = 0; 21 | yoffset = 0; 22 | } 23 | 24 | if (!(_.isUndefined(selector) || _.isString(selector)) && _.isNumber(xoffset) && _.isNumber(yoffset)) { 25 | reject(new Error('Input is invalid.')); 26 | } 27 | 28 | if (selector) { 29 | protocol.element.call(this, selector) 30 | .then(($el) => { 31 | let elOffset = $el.offset(); 32 | let rectLeft = elOffset.left + xoffset; 33 | let rectTop = elOffset.top + yoffset; 34 | 35 | window.scrollTo(rectLeft, rectTop); 36 | resolve(); 37 | }) 38 | .catch(reject); 39 | } else { 40 | window.scrollTo(xoffset, yoffset); 41 | resolve(); 42 | } 43 | }); 44 | } 45 | 46 | export default { 47 | pause, 48 | scroll, 49 | }; 50 | -------------------------------------------------------------------------------- /spec/integrated/non-vnext-spec/aggregate-row-spec.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import _ from 'underscore'; 3 | import pGrid from 'component/grid'; 4 | import chai from 'chai'; 5 | import util from 'util'; 6 | import driver from 'driver'; 7 | import peopleData from 'data/people.json'; 8 | 9 | let expect = chai.expect; 10 | let memoryDataSource = _.map(peopleData.value, (row) => { 11 | return _.pick(row, 'UserName', 'FirstName', 'LastName', 'Gender', 'Concurrency'); 12 | }); 13 | let memoryHeader = _.keys(_.first(memoryDataSource)); 14 | let memoryData = util.getExpectedGridData(memoryDataSource); 15 | let gridConfig = { 16 | el: '#container', 17 | dataSource: { 18 | type: 'memory', 19 | data: memoryData, 20 | }, 21 | columns: [ 22 | { 23 | name: 'UserName', 24 | }, 25 | { 26 | name: 'FirstName', 27 | }, 28 | { 29 | name: 'LastName', 30 | }, 31 | { 32 | name: 'Gender', 33 | }, 34 | ], 35 | }; 36 | 37 | let pgrid; 38 | let pgridFactory; 39 | let gridView; 40 | 41 | describe('aggregate row for non-vnext', function () { 42 | let bodyRowSelector = '#container .grid tbody .table__row--body'; 43 | 44 | beforeEach(function () { 45 | util.renderTestContainer(); 46 | pgridFactory = pGrid 47 | .factory(); 48 | }); 49 | 50 | afterEach(() => { 51 | gridView.remove(); 52 | util.cleanup(); 53 | }); 54 | 55 | it('aggregate should works as expected for non-vnext', function (done) { 56 | let aggregateConfig= { 57 | aggregate: { 58 | top() { 59 | return [ 60 | {UserName: 'Total Top'}, 61 | ]; 62 | }, 63 | bottom() { 64 | return [ 65 | {UserName: 'Total Bottom'}, 66 | ]; 67 | }, 68 | } 69 | }; 70 | 71 | gridView = pgridFactory 72 | .create(_.extend(aggregateConfig, gridConfig)) 73 | .gridView 74 | .render({fetch: true}); 75 | driver.element(bodyRowSelector) 76 | .then((result) => { 77 | let aggregateFirstText = result.first().find('td').first().text(); 78 | let aggregateLastText = result.last().find('td').first().text(); 79 | 80 | expect(aggregateFirstText).to.equal('Total Top'); 81 | expect(aggregateLastText).to.equal('Total Bottom'); 82 | }) 83 | .then(done) 84 | .catch(console.log); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /spec/integrated/non-vnext-spec/grid-view-api-spec.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import _ from 'underscore'; 3 | import pGrid from 'component/grid'; 4 | import chai from 'chai'; 5 | import util from 'util'; 6 | import driver from 'driver'; 7 | import peopleData from 'data/people.json'; 8 | 9 | let expect = chai.expect; 10 | let memoryDataSource = _.map(peopleData.value, (row) => { 11 | return _.pick(row, 'UserName', 'FirstName', 'LastName', 'Gender', 'Concurrency'); 12 | }); 13 | let memoryHeader = _.keys(_.first(memoryDataSource)); 14 | let memoryData = util.getExpectedGridData(memoryDataSource); 15 | let gridConfig = { 16 | el: '#container', 17 | dataSource: { 18 | type: 'memory', 19 | data: memoryData, 20 | }, 21 | columns: [ 22 | { 23 | name: 'UserName', 24 | }, 25 | { 26 | name: 'FirstName', 27 | }, 28 | { 29 | name: 'LastName', 30 | }, 31 | { 32 | name: 'Gender', 33 | }, 34 | ], 35 | }; 36 | 37 | let pgrid; 38 | let pgridFactory; 39 | let gridView; 40 | 41 | describe('grid api check for non-vnext', function () { 42 | let bodyRowSelector = '#container .grid tbody .table__row--body'; 43 | 44 | beforeEach(function () { 45 | util.renderTestContainer(); 46 | pgridFactory = pGrid 47 | .factory(); 48 | }); 49 | 50 | afterEach(() => { 51 | gridView.remove(); 52 | util.cleanup(); 53 | }); 54 | 55 | it('verify selection function for non-vnext', function (done) { 56 | let selectionConfig = { 57 | selectable: true, 58 | }; 59 | 60 | gridView = pgridFactory 61 | .create(_.extend(selectionConfig, gridConfig)) 62 | .gridView 63 | .render({fetch: true}); 64 | driver.element(bodyRowSelector) 65 | .then((result) => { 66 | return driver.click(util.getCheckboxElFromTbody(result, 0, 0)); 67 | }) 68 | .then(() => { 69 | let selectedItem = gridView.getSelection(); 70 | expect(selectedItem.length).to.equal(1); 71 | expect(selectedItem[0]).to.equal(1); 72 | }) 73 | .then(done) 74 | .catch(console.log); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /spec/integrated/non-vnext-spec/page-spec.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import _ from 'underscore'; 3 | import pGrid from 'component/grid'; 4 | import chai from 'chai'; 5 | import util from 'util'; 6 | import driver from 'driver'; 7 | import peopleData from 'data/people.json'; 8 | 9 | let expect = chai.expect; 10 | let memoryDataSource = _.map(peopleData.value, (row) => { 11 | return _.pick(row, 'UserName', 'FirstName', 'LastName', 'Gender', 'Concurrency'); 12 | }); 13 | let memoryHeader = _.keys(_.first(memoryDataSource)); 14 | let memoryData = util.getExpectedGridData(memoryDataSource); 15 | let gridConfig = { 16 | el: '#container', 17 | dataSource: { 18 | type: 'memory', 19 | data: memoryData, 20 | }, 21 | columns: [ 22 | { 23 | name: 'UserName', 24 | }, 25 | { 26 | name: 'FirstName', 27 | }, 28 | { 29 | name: 'LastName', 30 | }, 31 | { 32 | name: 'Gender', 33 | }, 34 | ], 35 | }; 36 | 37 | let pgrid; 38 | let pgridFactory; 39 | let gridView; 40 | 41 | describe('pageable config for non-vnext', function () { 42 | let bodyRowSelector = '#container .grid tbody .table__row--body'; 43 | 44 | beforeEach(function () { 45 | util.renderTestContainer(); 46 | pgridFactory = pGrid 47 | .factory(); 48 | }); 49 | 50 | afterEach(() => { 51 | gridView.remove(); 52 | util.cleanup(); 53 | }); 54 | 55 | it('pageable should works as expected for non-vnext', function (done) { 56 | let pageableConfig = { 57 | pageable: { 58 | pageSize: 5, 59 | pageSizes: [5, 10, 15, 20], 60 | }, 61 | }; 62 | 63 | gridView = pgridFactory 64 | .create(_.extend(pageableConfig, gridConfig)) 65 | .gridView 66 | .render({fetch: true}); 67 | driver.element(bodyRowSelector) 68 | .then((result) => { 69 | expect(result.length).to.equal(5); 70 | }) 71 | .then(done) 72 | .catch(console.log); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /spec/integrated/styles/tri-state-checkbox.css: -------------------------------------------------------------------------------- 1 | .column-tri-state-checkbox { 2 | border: 1px solid rgba(155,155,155,0.57); 3 | border-radius: 1px; 4 | height: 21px; 5 | margin: 0 1px; 6 | outline: 0; 7 | vertical-align: text-bottom; 8 | width: 21px; 9 | } 10 | 11 | .column-tri-state-checkbox > span.glyphicon { 12 | left: 3px; 13 | } 14 | -------------------------------------------------------------------------------- /spec/integrated/template/column-address.jade: -------------------------------------------------------------------------------- 1 | .column-address #{value} -------------------------------------------------------------------------------- /spec/integrated/template/column-header.jade: -------------------------------------------------------------------------------- 1 | .column-header=$text 2 | -------------------------------------------------------------------------------- /spec/unit/$speclist.js: -------------------------------------------------------------------------------- 1 | // require all `test/**/*.js` except for `test/**/$*.js` 2 | var testsContext = require.context('.', false, /[^\$][^\/]*\.js$/); 3 | testsContext.keys().forEach(testsContext); 4 | -------------------------------------------------------------------------------- /spec/unit/.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | "rules": 3 | "no-unused-expressions": 0 4 | -------------------------------------------------------------------------------- /spec/unit/aggregate-row-spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var AggregateRow = require('component/grid/projection/aggregate-row'); 3 | var Base = require('component/grid/projection/base'); 4 | var Response = require('component/grid/model/response'); 5 | 6 | describe('projection AggregateRow', function () { 7 | it('update should run normal', function () { 8 | var model = new AggregateRow({ 9 | 'aggregate.top': function (data) { 10 | return [{ 11 | name: data.get('value').length, 12 | $metadata: {}, 13 | }]; 14 | }, 15 | 16 | 'aggregate.bottom': function (data) { 17 | return [{ 18 | name: data.get('value').length, 19 | $metadata: {}, 20 | }]; 21 | }, 22 | }); 23 | 24 | var originalData = new Base(); 25 | originalData.data = new Response({ 26 | value: [ 27 | { name: 'hello', id: 'hello_id' }, 28 | { name: 'world', id: 'world_id' }, 29 | ], 30 | }); 31 | 32 | originalData.pipe(model); 33 | expect(model.data.get('value')[0].$metadata).to.be.eql({ type: 'aggregate' }); 34 | expect(model.data.get('value')[0].name).to.be.eql(2); 35 | 36 | expect(model.data.get('value')[3].$metadata).to.be.eql({ type: 'aggregate' }); 37 | expect(model.data.get('value')[3].name).to.be.eql(2); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /spec/unit/base-spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var sinon = require('sinon'); 3 | var Base = require('component/grid/projection/base'); 4 | 5 | describe('projection Base', function () { 6 | var ExtendModel, instance; 7 | beforeEach(function () { 8 | ExtendModel = Base.extend({ 9 | defaults: { attr1: 'attr1' }, 10 | }); 11 | instance = new ExtendModel(); 12 | }); 13 | 14 | it('constructor and initialize should run normal', function () { 15 | expect(instance.localKeys).to.be.deep.equal(["attr1"]); 16 | expect(instance.data).to.not.be.null; 17 | }); 18 | 19 | it('pipe should run normal', function () { 20 | var otherInstance = new ExtendModel(); 21 | sinon.spy(instance.data, 'off'); 22 | sinon.spy(instance, 'off'); 23 | sinon.spy(instance.data, 'on'); 24 | sinon.spy(instance, 'on'); 25 | sinon.spy(otherInstance, 'update'); 26 | instance.pipe(otherInstance); 27 | 28 | expect(instance.data.off.called).to.be.true; 29 | expect(instance.off.called).to.be.true; 30 | expect(instance.data.on.called).to.be.true; 31 | expect(instance.on.called).to.be.true; 32 | expect(otherInstance.update.called).to.be.true; 33 | }); 34 | 35 | it('set should run normal', function () { 36 | var otherInstance = new ExtendModel(); 37 | sinon.spy(otherInstance, 'beforeSet'); 38 | sinon.spy(otherInstance, 'afterSet'); 39 | 40 | instance.pipe(otherInstance); 41 | otherInstance.set({ attr1: 'value1', attr2: 'value2' }); 42 | 43 | expect(otherInstance.beforeSet.calledWith({ attr1: 'value1' }, { attr2: 'value2' })).to.true; 44 | expect(otherInstance.afterSet.called).to.be.true; 45 | expect(otherInstance.get('attr1')).to.equal('value1'); 46 | }); 47 | 48 | it('bubble should run normal', function () { 49 | var otherInstance = new ExtendModel(); 50 | sinon.stub(instance, 'bubble'); 51 | if (!otherInstance.events) { 52 | otherInstance.events = {}; 53 | } 54 | otherInstance.events['test-event'] = 'testEvent'; 55 | otherInstance.testEvent = sinon.spy(); 56 | 57 | instance.pipe(otherInstance); 58 | otherInstance.bubble('test-event', 'arguments'); 59 | expect(otherInstance.testEvent.calledWith('arguments')).to.be.true; 60 | expect(instance.bubble.calledWith('test-event', 'arguments')); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /spec/unit/column-i18n-spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var ColumnI18n = require('component/grid/projection/column-i18n'); 3 | var Base = require('component/grid/projection/base'); 4 | var Response = require('component/grid/model/response'); 5 | 6 | describe('projection ColumnI18n', function () { 7 | it('update should run normal', function () { 8 | var model = new ColumnI18n({ 9 | 'column.i18n': { 10 | 'name': 'Name', 11 | '': function (name) { 12 | return '$' + name; 13 | }, 14 | }, 15 | 'subColumn.i18n': { 16 | name: 'SubName', 17 | }, 18 | }); 19 | var originalData = new Base(); 20 | originalData.data = new Response({ 21 | columns: { 22 | name: { name: 'hello', property: 'name' }, 23 | id: { id: '007', property: 'id' }, 24 | }, 25 | }); 26 | originalData.pipe(model); 27 | expect(model.data.get('columns').name.$text).to.be.equal('Name'); 28 | expect(model.data.get('columns').name.config.subColTitle).to.be.equal('SubName'); 29 | expect(model.data.get('columns').id.$text).to.be.equal('$id'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /spec/unit/column-queryable-spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var ColumnQueryable = require('component/grid/projection/column-queryable'); 3 | var Base = require('component/grid/projection/base'); 4 | var Response = require('component/grid/model/response'); 5 | 6 | describe('projection ColumnQueryable', function () { 7 | it('update should run normal', function () { 8 | var model = new ColumnQueryable({ 9 | 'column.take': 2, 10 | 'column.skip': 1, 11 | 'column.lock': ['name'], 12 | }); 13 | 14 | var originalData = new Base(); 15 | originalData.data = new Response({ 16 | columns: { 17 | name: { name: 'hello', property: 'name' }, 18 | id: { id: '007', property: 'id' }, 19 | attr1: { attr1: '007', property: 'attr1' }, 20 | attr2: { attr2: '007', property: 'attr2' }, 21 | }, 22 | }); 23 | originalData.pipe(model); 24 | expect(model.data.get('columns').name.$lock).to.be.equal(true); 25 | expect(model.data.get('select').length).to.be.equal(2); 26 | 27 | expect(model.data.get('column.skip')).to.be.equal(1); 28 | expect(model.data.get('columns.skipped')).to.be.eql(['id']); 29 | expect(model.data.get('columns.remaining')).to.be.eql(['attr2']); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /spec/unit/column-shifter-spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var sinon = require('sinon'); 3 | var ColumnShifter = require('component/grid/projection/column-shifter'); 4 | var Base = require('component/grid/projection/base'); 5 | var Response = require('component/grid/model/response'); 6 | 7 | describe('projection ColumnShifter', function () { 8 | it('update should run normal', function () { 9 | var model = new ColumnShifter(); 10 | 11 | var originalData = new Base(); 12 | originalData.data = new Response({ 13 | columns: { 14 | name: { name: 'hello', property: 'name' }, 15 | id: { id: '007', property: 'id' }, 16 | }, 17 | select: ['name', 'id'], 18 | }); 19 | originalData.pipe(model); 20 | expect(model.data.get('select')[0]).to.be.equal('column.skip.less'); 21 | expect(model.data.get('select')[3]).to.be.equal('column.skip.more'); 22 | }); 23 | 24 | it('thClick should run normal', function () { 25 | var model = new ColumnShifter(); 26 | model.get = sinon.stub().returns(1); 27 | sinon.spy(model, 'set'); 28 | 29 | model.thClick({}, { 30 | column: { $metadata: { enabled: true } }, 31 | property: 'column.skip.less', 32 | }); 33 | expect(model.set.calledWith({ 'column.skip': 0 })).to.be.true; 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /spec/unit/conf/karma.conf.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var path = require('path'); 3 | 4 | function getWebpackConfig() { 5 | var webpackConfig = require('./webpack.config'); 6 | 7 | webpackConfig.module.preLoaders = [ 8 | { 9 | test: /\.js$/, 10 | include: path.resolve('./js/'), 11 | loader: 'istanbul-instrumenter', 12 | }, 13 | ].concat(webpackConfig.module.preLoaders || []); 14 | return webpackConfig; 15 | } 16 | 17 | module.exports = function (config) { 18 | config.set({ 19 | basePath: '../../../', 20 | frameworks: [ 21 | 'mocha', 22 | ], 23 | client: { 24 | mocha: { 25 | reporter: 'html', // change Karma's debug.html to the mocha web reporter 26 | }, 27 | }, 28 | reporters: ['mocha', 'coverage', 'junit'], 29 | webpack: getWebpackConfig(), 30 | coverageReporter: { 31 | dir: './spec/unit/coverage', 32 | reporters: [ 33 | { type: 'html', subdir: 'report-html' }, 34 | { type: 'lcov', subdir: 'report-lcov' }, 35 | ], 36 | }, 37 | junitReporter: { 38 | outputDir: './spec/unit/test-results/', 39 | }, 40 | browsers: [ 41 | 'PhantomJS', 42 | ], 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /spec/unit/conf/karma.debug.conf.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var path = require('path'); 3 | var webpackConfig = require('./webpack.config'); 4 | 5 | module.exports = function (config) { 6 | config.set({ 7 | basePath: '../../../', 8 | frameworks: [ 9 | 'mocha', 10 | ], 11 | reporters: [ 12 | 'mocha', 13 | ], 14 | client: { 15 | mocha: { 16 | reporter: 'html', // change Karma's debug.html to the mocha web reporter 17 | }, 18 | }, 19 | webpack: webpackConfig, 20 | browsers: [ 21 | 'ChromeNoSandbox', 22 | ], 23 | customLaunchers: { 24 | ChromeNoSandbox: { 25 | base: 'Chrome', 26 | flags: ['--no-sandbox'], 27 | }, 28 | }, 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /spec/unit/conf/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var _ = require('underscore'); 3 | var webpackAlias = require('../../../webpack.alias'); 4 | _.extend(webpackAlias, {'sinon': 'sinon/pkg/sinon.js'}); 5 | 6 | module.exports = { 7 | output: { 8 | path: path.resolve('./dist'), 9 | filename: 'unit-test.js', 10 | devtoolModuleFilenameTemplate: function (info) { 11 | if (path.isAbsolute(info.absoluteResourcePath)) { 12 | return 'webpack-src:///unit-test/' + path.relative('.', info.absoluteResourcePath).replace(/\\/g, '/'); 13 | } 14 | return info.absoluteResourcePath; 15 | }, 16 | }, 17 | resolve: { 18 | alias: webpackAlias, 19 | }, 20 | module: { 21 | loaders: [ 22 | { 23 | test: /sinon\.js$/, 24 | loader: 'imports?define=>false,require=>false', 25 | }, 26 | // jade 27 | { test: /\.jade$/, loader: 'jade-loader' }, 28 | // jade-end 29 | // es2015 30 | { test: /\.js$/, exclude: /\bnode_modules\b/, loader: 'babel-loader' }, 31 | // es2015-end 32 | { test: /\.less$/, loader: 'style!css!less' }, 33 | ], 34 | 35 | }, 36 | babel: { presets: ['es2015'] }, 37 | devtool: 'inline-source-map', 38 | }; 39 | 40 | -------------------------------------------------------------------------------- /spec/unit/jsdata-spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var sinon = require('sinon'); 3 | var Promise = require('bluebird'); 4 | var JSDataProjection = require('component/grid/projection/jsdata'); 5 | var Base = require('component/grid/projection/base'); 6 | 7 | chai.use(require('sinon-chai')); 8 | var expect = chai.expect; 9 | 10 | describe('JSData Projection', function () { 11 | var MockEntity = function () { 12 | this.findAll = sinon.spy(function () { 13 | return Promise.resolve([]); 14 | }); 15 | }; 16 | 17 | var entity = null; 18 | var projection = null; 19 | 20 | beforeEach(function () { 21 | entity = new MockEntity(); 22 | projection = new JSDataProjection({ 23 | 'jsdata.entity': entity, 24 | 'jsdata.options': {}, 25 | 'jsdata.skip': 10, 26 | 'jsdata.take': 10, 27 | 'jsdata.filter': {}, 28 | 'jsdata.orderby': [], 29 | 'jsdata.select': [], 30 | }); 31 | }); 32 | 33 | it('should be a projection', function () { 34 | expect(projection).to.be.instanceof(Base); 35 | }); 36 | 37 | describe('update', function () { 38 | it('should call findAll of the entity and finish', function (done) { 39 | var onUpdateBegin = sinon.spy(); 40 | projection.on('update:beginning', onUpdateBegin); 41 | projection.on('update:finished', function () { 42 | expect(onUpdateBegin).to.have.been.called; 43 | done(); 44 | }); 45 | projection.update(); 46 | expect(entity.findAll).to.have.been.called; 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /spec/unit/map-spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var sinon = require('sinon'); 3 | var MapProjection = require('component/grid/projection/map'); 4 | var Base = require('component/grid/projection/base'); 5 | var Response = require('component/grid/model/response'); 6 | 7 | describe('projection map', function () { 8 | it('update should run normal', function () { 9 | var model = new MapProjection({ 10 | map: function (item) { 11 | return [{ name: item.name }]; 12 | }, 13 | }); 14 | 15 | var originalData = new Base(); 16 | originalData.data = new Response({ 17 | value: [{ name: 'hello' }], 18 | }); 19 | sinon.spy(model, 'patch'); 20 | originalData.pipe(model); 21 | expect(model.patch.called).to.be.true; 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /spec/unit/mock-spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var Mock = require('component/grid/projection/mock'); 3 | var Base = require('component/grid/projection/base'); 4 | var Response = require('component/grid/model/response'); 5 | 6 | describe('projection Mock', function () { 7 | it('update should run normal', function () { 8 | var model = new Mock({ n: 10 }); 9 | 10 | var originalData = new Base(); 11 | originalData.data = new Response(); 12 | 13 | originalData.pipe(model); 14 | expect(model.data.get('value').length).to.be.equal(10); 15 | expect(model.data.get('count')).to.be.equal(10); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /spec/unit/odata-spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var sinon = require('sinon'); 3 | var $ = require('lib/jquery'); 4 | var OData = require('component/grid/projection/odata'); 5 | 6 | describe('projection OData', function () { 7 | it('constructor and initialize should run normal', function () { 8 | var model = new OData({ 9 | url: 'www.bing.com', 10 | skip: 10, 11 | take: 10, 12 | orderby: [{ name: 1 }], 13 | filter: 'name', 14 | }); 15 | sinon.stub($, 'getJSON', function () { 16 | var obj = { 17 | success: function () { 18 | return obj; 19 | }, 20 | error: function () { 21 | return obj; 22 | }, 23 | complete: function () {}, 24 | }; 25 | return obj; 26 | }); 27 | model.update(); 28 | var expectRes = { 29 | $format: 'json', 30 | $count: true, 31 | $top: 10, 32 | $skip: 10, 33 | $orderby: 'name asc', 34 | }; 35 | expect($.getJSON.calledWith('www.bing.com', expectRes)).to.be.true; 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /spec/unit/page-spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var Page = require('component/grid/projection/page'); 3 | var Base = require('component/grid/projection/base'); 4 | var Response = require('component/grid/model/response'); 5 | 6 | describe('projection Page', function () { 7 | it('update should run normal', function () { 8 | var model = new Page({ 9 | 'page.size': 200, 10 | 'page.number': 0, 11 | }); 12 | 13 | var originalData = new Base(); 14 | originalData.data = new Response({ count: 400 }); 15 | originalData.pipe(model); 16 | expect(model.data.get('page.count')).to.be.equal(2); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /spec/unit/property-template-spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var PropertyTemplate = require('component/grid/projection/property-template'); 3 | var Base = require('component/grid/projection/base'); 4 | var Response = require('component/grid/model/response'); 5 | 6 | describe('projection PropertyTemplate', function () { 7 | it('update should run normal', function () { 8 | var model = new PropertyTemplate({ 9 | 'property.template': { 10 | name: function (local) { 11 | return local.model[local.property]; 12 | }, 13 | id: function (local) { 14 | return local.model[local.property]; 15 | }, 16 | }, 17 | }); 18 | 19 | var originalData = new Base(); 20 | originalData.data = new Response({ 21 | value: [ 22 | { name: 'hello', id: 'hello_id' }, 23 | { name: 'world', id: 'world_id' }, 24 | ], 25 | }); 26 | originalData.pipe(model); 27 | expect(model.data.get('value')[0].name.$html).to.be.equal('hello'); 28 | expect(model.data.get('value')[0].id.$html).to.be.equal('hello_id'); 29 | 30 | expect(model.data.get('value')[1].name.$html).to.be.equal('world'); 31 | expect(model.data.get('value')[1].id.$html).to.be.equal('world_id'); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /spec/unit/row-checkbox-spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var RowCheckbox = require('component/grid/projection/row-checkbox'); 3 | var Base = require('component/grid/projection/base'); 4 | var Response = require('component/grid/model/response'); 5 | 6 | describe('projection RowCheckbox', function () { 7 | it('update should run normal', function () { 8 | var model = new RowCheckbox({ 9 | 'row.check.id': 'id', 10 | 'column.checked': 'name', 11 | }); 12 | 13 | var originalData = new Base(); 14 | originalData.data = new Response({ 15 | value: [ 16 | { name: 'hello', id: 'hello_id' }, 17 | { name: 'world', id: 'world_id' }, 18 | ], 19 | columns: [ 20 | { name: 'hello', property: 'name' }, 21 | { id: 'hello_id', property: 'id' }, 22 | ], 23 | }); 24 | 25 | originalData.pipe(model); 26 | expect(model.data.get('value')[0].name.$html).to.be.not.null; 27 | expect(model.data.get('value')[1].name.$html).to.be.not.null; 28 | }); 29 | 30 | it('thClick should run normal', function () { 31 | var model = new RowCheckbox({ 32 | 'row.check.id': 'id', 33 | 'column.checked': 'name', 34 | 'row.check.allow': true, 35 | }); 36 | model.data = new Response({ 37 | value: [ 38 | { name: 'hello', id: 'hello_id' }, 39 | { name: 'world', id: 'world_id' }, 40 | ], 41 | }); 42 | model.thClick({}, { property: 'name', checked: true }); 43 | expect(model.get('row.check.list')).to.be.eql(['hello_id', 'world_id']); 44 | }); 45 | 46 | it('tdClick should run normal', function () { 47 | var model = new RowCheckbox({ 48 | 'row.check.id': 'id', 49 | 'column.checked': 'name', 50 | 'row.check.list': [], 51 | }); 52 | var newModel = new Response({ name: 'hello', id: 'hello_id' }); 53 | model.tdClick({}, { property: 'name', model: newModel, checked: true }); 54 | 55 | expect(model.get('row.check.list')).to.be.eql(['hello_id']); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /spec/unit/row-index-spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var RowIndex = require('component/grid/projection/row-index'); 3 | var Base = require('component/grid/projection/base'); 4 | var Response = require('component/grid/model/response'); 5 | 6 | describe('projection RowIndex', function () { 7 | it('update should run normal', function () { 8 | var model = new RowIndex(); 9 | 10 | var originalData = new Base(); 11 | originalData.data = new Response({ 12 | value: [ 13 | { name: 'hello', id: 'hello_id' }, 14 | { name: 'world', id: 'world_id' }, 15 | ], 16 | }); 17 | 18 | originalData.pipe(model); 19 | expect(model.data.get('value')[0].rowIndex).to.be.equal(1); 20 | expect(model.data.get('value')[1].rowIndex).to.be.equal(2); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /spec/unit/util.js: -------------------------------------------------------------------------------- 1 | import { flow, omit, isEqual } from 'lodash/fp'; 2 | import sinon from 'sinon'; 3 | import $ from 'lib/jquery'; 4 | 5 | export var $container = $('body > #container'); 6 | if ($container.length === 0) { 7 | $container = $('
').appendTo('body'); 8 | } 9 | 10 | export function cleanup() { 11 | $container.empty(); 12 | } 13 | 14 | export function objectPartialMatch(propsToOmit, expected) { 15 | const omitProps = omit(propsToOmit); 16 | return sinon.match( 17 | flow( 18 | // function (actual) { 19 | // console.log('Actual', JSON.stringify(omitProps(actual))); 20 | // console.log('Expected', JSON.stringify(expected)); 21 | // return actual; 22 | // }, 23 | omitProps, 24 | isEqual(expected) 25 | // result => { 26 | // console.log('result', result); 27 | // return result; 28 | // } 29 | ), 'matching ' + JSON.stringify(expected) + ' without [' + propsToOmit.map(prop => `"${prop}"`).join(', ') + '] properties'); 30 | } 31 | -------------------------------------------------------------------------------- /spec/util/index.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import $ from 'jquery'; 3 | 4 | let $container; 5 | 6 | function renderTestContainer(id) { 7 | id = id || 'container'; 8 | $container = $(id); 9 | if ($container.length === 0) { 10 | $container = $('
')).appendTo('body'); 11 | } 12 | return $container; 13 | } 14 | 15 | function cleanup() { 16 | $container.empty(); 17 | } 18 | 19 | function createGrid(pgridFactory, config, extendedConfig) { 20 | 21 | } 22 | 23 | function validateElementArray(elementArray, expectedData) { 24 | let actualArray = _.map(elementArray, (element) => { 25 | return element.textContent; 26 | }); 27 | 28 | return _.isEqual(actualArray, expectedData); 29 | } 30 | 31 | function validateElementMatrix(elementMatrix, expectedData) { 32 | if (elementMatrix.length !== expectedData.length) { 33 | return false; 34 | } 35 | let validation = true; 36 | _.forEach(elementMatrix, (elementRow, index) => { 37 | let expectedArray = _.values(expectedData[index]); 38 | 39 | expectedArray = _.map(expectedArray, (item) => { 40 | return String(item); 41 | }); 42 | if (!validateElementArray(elementRow.children, expectedArray)) { 43 | validation = false; 44 | } 45 | }); 46 | return validation; 47 | } 48 | 49 | function getExpectedGridData(data, columnKeys) { 50 | return _.map(data, (row) => { 51 | return columnKeys ? _.pick(row, columnKeys) : row; 52 | }); 53 | } 54 | 55 | function validateClassesForElement(el, classes) { 56 | let assertion = classes.every((clazz) => { 57 | return el.hasClass(clazz); 58 | }); 59 | return assertion; 60 | } 61 | 62 | function validateClassesForElementArray(elArray, classes) { 63 | let assertion = elArray.every((el) => { 64 | return validateClassesForElement(el, classes); 65 | }); 66 | return assertion; 67 | } 68 | 69 | function getCheckboxElFromTable($el, selector, xindex, yindex) { 70 | let $checkbox = $el.eq(xindex).find(selector).eq(yindex).find('input'); 71 | return $checkbox; 72 | } 73 | 74 | function getCheckboxElFromThead($el, xindex, yindex) { 75 | return getCheckboxElFromTable($el, 'th', xindex, yindex); 76 | } 77 | 78 | function getCheckboxElFromTbody($el, xindex, yindex) { 79 | return getCheckboxElFromTable($el, 'td', xindex, yindex); 80 | } 81 | 82 | export default { 83 | renderTestContainer, 84 | cleanup, 85 | validateElementMatrix, 86 | getExpectedGridData, 87 | validateClassesForElement, 88 | validateClassesForElementArray, 89 | getCheckboxElFromThead, 90 | getCheckboxElFromTbody, 91 | }; 92 | -------------------------------------------------------------------------------- /wdio.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var democase = require('democase'); 3 | 4 | var demoSet = democase.loadSync(path.resolve(__dirname, 'demos')); 5 | var config = demoSet.wdioConfig({ 6 | capabilities: [{ 7 | browserName: 'firefox', 8 | }], 9 | reporters: ['dot', 'junit'], 10 | reporterOptions: { outputDir: './test-results/' }, 11 | }); 12 | 13 | module.exports = { config: config }; 14 | -------------------------------------------------------------------------------- /webpack.alias.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | // Config your webpack resolve.alias in this file 3 | module.exports = { 4 | 'lib/underscore': 'underscore', 5 | 'lib/backbone': 'backbone', 6 | 'lib/jquery': 'jquery', 7 | 'component/grid': path.resolve('./js'), 8 | 'component/popup-editor': path.resolve('./js/popup-editor'), 9 | }; 10 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var path = require('path'); 3 | var pkg = require('./package'); 4 | 5 | var webpackAlias = pkg.webpackAlias || {}; 6 | 7 | try { 8 | webpackAlias = require('./webpack.alias'); 9 | } catch (e) { } 10 | 11 | function getExternals() { 12 | var deps = _.keys(pkg.peerDependencies); 13 | var externals = _.object(deps, deps); 14 | 15 | return _.reduce(_.pairs(webpackAlias), function (exts, pair) { 16 | if (_.has(externals, pair[1])) { 17 | exts[pair[0]] = pair[1]; 18 | } 19 | return exts; 20 | }, externals); 21 | } 22 | 23 | module.exports = { 24 | entry: path.resolve('./js/index.js'), 25 | output: { 26 | path: path.join(__dirname, 'dist'), 27 | filename: 'projection-grid.js', 28 | library: 'projection-grid', 29 | libraryTarget: 'umd', 30 | umdNamedDefine: false, 31 | devtoolModuleFilenameTemplate: function (info) { 32 | if (path.isAbsolute(info.absoluteResourcePath)) { 33 | return 'webpack-src:///projection-grid/' + path.relative('.', info.absoluteResourcePath).replace(/\\/g, '/'); 34 | } 35 | return info.absoluteResourcePath; 36 | }, 37 | }, 38 | module: { 39 | loaders: [ 40 | // jade 41 | { test: /\.jade$/, loader: 'babel!jade' }, 42 | // jade-end 43 | // es2015 44 | { test: /\.js$/, exclude: /\bnode_modules\b/, loader: 'babel-loader' }, 45 | // es2015-end 46 | { test: /\.less$/, loader: 'style!css!less' }, 47 | { test: /\.json$/, loader: 'json' }, 48 | ], 49 | }, 50 | babel: { presets: ['es2015'] }, 51 | externals: [getExternals()], 52 | resolve: { alias: webpackAlias }, 53 | devtool: 'source-map', 54 | }; 55 | 56 | --------------------------------------------------------------------------------