├── .gitignore ├── src ├── components │ ├── menu │ │ ├── context │ │ │ ├── ContextMenuController.js │ │ │ ├── ContextMenuDirective.js │ │ │ └── contextMenu.js │ │ ├── dropdown │ │ │ ├── DropdownController.js │ │ │ ├── DropdownMenuDirective.js │ │ │ ├── dropdown.js │ │ │ ├── DropdownToggleDirective.js │ │ │ └── DropdownDirective.js │ │ ├── menu.js │ │ ├── menu.less │ │ ├── MenuController.js │ │ └── MenuDirective.js │ ├── body │ │ ├── SelectionDirective.js │ │ ├── GroupRowController.js │ │ ├── StyleTranslator.js │ │ ├── GroupRowDirective.js │ │ ├── RowController.js │ │ ├── CellController.js │ │ ├── ScrollerDirective.js │ │ ├── BodyDirective.js │ │ ├── CellDirective.js │ │ ├── RowDirective.js │ │ └── SelectionController.js │ ├── popover │ │ ├── popover.js │ │ ├── PopoverRegistry.js │ │ ├── PositionHelper.js │ │ └── popover.less │ ├── footer │ │ ├── FooterController.js │ │ ├── FooterDirective.js │ │ ├── PagerDirective.js │ │ └── PagerController.js │ ├── DataTableService.spec.js │ ├── header │ │ ├── HeaderCellController.js │ │ ├── ResizableDirective.js │ │ ├── SortableDirective.js │ │ ├── HeaderController.js │ │ ├── HeaderCellDirective.js │ │ └── HeaderDirective.js │ ├── DataTableService.js │ └── DataTableDirective.js ├── utils │ ├── utils.spec.js │ ├── keys.js │ ├── translate.js │ ├── polyfill.js │ ├── vendorPrefixes.js │ ├── throttle.js │ ├── utils.js │ └── math.js ├── tests │ └── frameworks.js ├── dataTable.js ├── dataTable.less └── defaults.js ├── jsconfig.json ├── .editorconfig ├── .eslintrc ├── .travis.yml ├── bower.json ├── karma.conf.js ├── config.js ├── LICENSE ├── CONTRIBUTING.md ├── demos ├── cell-templates.html ├── virtual.html ├── basic.html ├── slow.html ├── empty.html ├── scroll.html ├── greed.html ├── action-links.html ├── grouping.html ├── expressive.html ├── paging.html ├── tabs.html ├── virtual-paging.html ├── force.html ├── updating.html ├── perf.html ├── sort.html ├── inline-editing.html ├── columnadd.html ├── single-select.html ├── perf-horzscroll.html ├── checkboxes.html ├── transclude.html ├── tall.html ├── filters.html ├── pins.html └── tooltip.html ├── package.json ├── release └── dataTable.css ├── index.html └── gulpfile.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | jspm_packages 3 | npm-debug.log 4 | dist 5 | .idea 6 | -------------------------------------------------------------------------------- /src/components/menu/context/ContextMenuController.js: -------------------------------------------------------------------------------- 1 | export class ContextMenuController{ 2 | 3 | } 4 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "commonjs", 5 | "diagnostics": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators":true 8 | } 9 | } -------------------------------------------------------------------------------- /src/components/menu/context/ContextMenuDirective.js: -------------------------------------------------------------------------------- 1 | export function ContextMenuDirective(){ 2 | return { 3 | restrict: 'C', 4 | controller: 'ContextMenuController', 5 | link: function($scope, $elm, $attrs, ctrl) { 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/components/menu/dropdown/DropdownController.js: -------------------------------------------------------------------------------- 1 | export class DropdownController{ 2 | /*@ngInject*/ 3 | constructor($scope){ 4 | $scope.open = false; 5 | } 6 | 7 | toggle(scope){ 8 | scope.open = !scope.open; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/menu/context/contextMenu.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import dropdown from '../dropdown/dropdown'; 3 | 4 | export default angular 5 | .module('contextmenu', [ dropdown.name ]) 6 | .directive('contextMenu', ContextMenuDirective) 7 | -------------------------------------------------------------------------------- /src/utils/utils.spec.js: -------------------------------------------------------------------------------- 1 | import '../tests/frameworks'; 2 | 3 | import {ObjectId} from './utils'; 4 | 5 | describe('utils', function () { 6 | it('should generate unique ID', () => { 7 | var id1 = ObjectId(); 8 | var id2 = ObjectId(); 9 | 10 | id1.should.not.eq(id2); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/components/body/SelectionDirective.js: -------------------------------------------------------------------------------- 1 | import { SelectionController } from './SelectionController'; 2 | 3 | export function SelectionDirective(){ 4 | return { 5 | controller: SelectionController, 6 | restrict: 'A', 7 | require:'^dtBody', 8 | controllerAs: 'selCtrl' 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # 2 space indentation 12 | [**.*] 13 | indent_style = space 14 | indent_size = 2 -------------------------------------------------------------------------------- /src/components/menu/menu.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import { MenuController } from './MenuController'; 3 | import { MenuDirective } from './MenuDirective'; 4 | import dropdown from './dropdown/dropdown'; 5 | 6 | export default angular 7 | .module('dt.menu', [ dropdown.name ]) 8 | .controller('MenuController', MenuController) 9 | .directive('dtm', MenuDirective); 10 | -------------------------------------------------------------------------------- /src/components/menu/dropdown/DropdownMenuDirective.js: -------------------------------------------------------------------------------- 1 | export function DropdownMenuDirective($animate){ 2 | return { 3 | restrict: 'C', 4 | require: '?^dropdown', 5 | link: function($scope, $elm, $attrs, ctrl) { 6 | $scope.$watch('open', () => { 7 | $animate[$scope.open ? 'addClass' : 'removeClass']($elm, 'ddm-open'); 8 | }); 9 | } 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /src/tests/frameworks.js: -------------------------------------------------------------------------------- 1 | import 'babel-core/polyfill'; 2 | import chai from 'chai'; 3 | import sinonChai from 'sinon-chai'; 4 | import sinon from 'sinon'; 5 | import chaiAsPromised from 'chai-as-promised'; 6 | 7 | chai.use(chaiAsPromised); 8 | chai.use(sinonChai); 9 | 10 | chai.should(); 11 | 12 | let expect = chai.expect; 13 | 14 | export default chai; 15 | export {expect, sinon}; 16 | -------------------------------------------------------------------------------- /src/components/menu/menu.less: -------------------------------------------------------------------------------- 1 | .dt-menu{ 2 | text-align:left; 3 | 4 | ul, li{ 5 | padding:0; 6 | margin:0; 7 | list-style:none; 8 | } 9 | 10 | .dropdown-menu{ 11 | width:200px; 12 | display: none; 13 | 14 | &.ddm-open{ 15 | display: inline-block; 16 | } 17 | } 18 | 19 | ul{ 20 | max-height: 300px; 21 | overflow-y: auto; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/body/GroupRowController.js: -------------------------------------------------------------------------------- 1 | export class GroupRowController { 2 | 3 | onGroupToggled(evt){ 4 | evt.stopPropagation(); 5 | this.onGroupToggle({ 6 | group: this.row 7 | }); 8 | } 9 | 10 | treeClass(){ 11 | return { 12 | 'dt-tree-toggle': true, 13 | 'icon-right': !this.expanded, 14 | 'icon-down': this.expanded 15 | }; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true 5 | }, 6 | "rules": { 7 | "strict": 0, 8 | "no-multi-spaces": false, 9 | "quotes": "single", 10 | "no-shadow": false, 11 | "no-empty": false, 12 | "no-cond-assign": false, 13 | "no-loop-func": false, 14 | "no-underscore-dangle": false, 15 | "no-use-before-define": false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/popover/popover.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import { PopoverDirective } from './PopoverDirective'; 3 | import { PopoverRegistry } from './PopoverRegistry'; 4 | import { PositionHelper } from './PositionHelper'; 5 | 6 | export default angular 7 | .module('popover', []) 8 | .service('PopoverRegistry', PopoverRegistry) 9 | .factory('PositionHelper', PositionHelper) 10 | .directive('popover', PopoverDirective); 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.12' 4 | 5 | branches: 6 | only: 7 | - master 8 | 9 | before_install: 10 | - export CHROME_BIN=chromium-browser 11 | - "export DISPLAY=:99.0" 12 | - "sh -e /etc/init.d/xvfb start" 13 | - npm install -g gulp jspm # this is done on before install due to npm install script in `package.json` 14 | 15 | script: 16 | - gulp test 17 | 18 | after_success: 19 | - test $TRAVIS_TEST_RESULT = 0 20 | && gulp release -------------------------------------------------------------------------------- /src/utils/keys.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Shortcut for key handlers 3 | * @type {Object} 4 | */ 5 | export var KEYS = { 6 | BACKSPACE: 8, 7 | TAB: 9, 8 | RETURN: 13, 9 | ALT: 18, 10 | ESC: 27, 11 | SPACE: 32, 12 | PAGE_UP: 33, 13 | PAGE_DOWN: 34, 14 | END: 35, 15 | HOME: 36, 16 | LEFT: 37, 17 | UP: 38, 18 | RIGHT: 39, 19 | DOWN: 40, 20 | DELETE: 46, 21 | COMMA: 188, 22 | PERIOD: 190, 23 | A: 65, 24 | Z: 90, 25 | ZERO: 48, 26 | NUMPAD_0: 96, 27 | NUMPAD_9: 105 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/menu/dropdown/dropdown.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import { DropdownController } from './DropdownController'; 3 | import { DropdownDirective } from './DropdownDirective'; 4 | import { DropdownToggleDirective } from './DropdownToggleDirective'; 5 | import { DropdownMenuDirective } from './DropdownMenuDirective'; 6 | 7 | export default angular 8 | .module('dt.dropdown', []) 9 | .controller('DropdownController', DropdownController) 10 | .directive('dropdown', DropdownDirective) 11 | .directive('dropdownToggle', DropdownToggleDirective) 12 | .directive('dropdownMenu', DropdownMenuDirective); 13 | -------------------------------------------------------------------------------- /src/components/menu/MenuController.js: -------------------------------------------------------------------------------- 1 | export class MenuController{ 2 | 3 | /*@ngInject*/ 4 | constructor($scope, $timeout){ 5 | this.$scope = $scope; 6 | } 7 | 8 | getColumnIndex(model){ 9 | return this.$scope.current.findIndex((col) => { 10 | return model.name == col.name; 11 | }); 12 | } 13 | 14 | isChecked(model){ 15 | return this.getColumnIndex(model) > -1; 16 | } 17 | 18 | onCheck(model){ 19 | var idx = this.getColumnIndex(model); 20 | if(idx === -1){ 21 | this.$scope.current.push(model); 22 | } else { 23 | this.$scope.current.splice(idx, 1); 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/components/menu/dropdown/DropdownToggleDirective.js: -------------------------------------------------------------------------------- 1 | export function DropdownToggleDirective($timeout){ 2 | return { 3 | restrict: 'C', 4 | controller: 'DropdownController', 5 | require: '?^dropdown', 6 | link: function($scope, $elm, $attrs, ctrl) { 7 | 8 | function toggleClick(event) { 9 | event.preventDefault(); 10 | $timeout(() => { 11 | ctrl.toggle($scope); 12 | }); 13 | }; 14 | 15 | function toggleDestroy(){ 16 | $elm.unbind('click', toggleClick); 17 | }; 18 | 19 | $elm.bind('click', toggleClick); 20 | $scope.$on('$destroy', toggleDestroy); 21 | } 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-data-table", 3 | "version": "0.7.0", 4 | "homepage": "https://github.com/Swimlane/angular-data-table", 5 | "author": { 6 | "name": "Swimlane", 7 | "email": "austin@swimlane.com", 8 | "web": "http://swimlane.com/" 9 | }, 10 | "main": [ 11 | "release/dataTable.js", 12 | "release/dataTable.css" 13 | ], 14 | "description": "A feature-rich but lightweight ES6 AngularJS Data Table crafted for large data sets!", 15 | "keywords": [ 16 | "data-table", 17 | "material-design", 18 | "angular", 19 | "table", 20 | "grid" 21 | ], 22 | "license": "MIT", 23 | "ignore": [ 24 | "**/.*", 25 | "jspm_packages", 26 | "node_modules", 27 | "bower_components", 28 | "test", 29 | "tests" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/components/popover/PopoverRegistry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Registering to deal with popovers 3 | * @param {function} $animate 4 | */ 5 | export function PopoverRegistry($animate){ 6 | var popovers = {}; 7 | this.add = function(id, object){ 8 | popovers[id] = object; 9 | } 10 | this.find = function(id){ 11 | popovers[id]; 12 | } 13 | this.remove = function(id){ 14 | delete popovers[id]; 15 | } 16 | this.removeGroup = function(group, currentId){ 17 | angular.forEach(popovers, function(popoverOb, id){ 18 | if (id === currentId) return; 19 | 20 | if (popoverOb.group && popoverOb.group === group){ 21 | $animate.removeClass(popoverOb.popover, 'sw-popover-animate').then(() => { 22 | popoverOb.popover.remove(); 23 | delete popovers[id]; 24 | }); 25 | } 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/footer/FooterController.js: -------------------------------------------------------------------------------- 1 | export class FooterController { 2 | 3 | /** 4 | * Creates an instance of the Footer Controller 5 | * @param {scope} 6 | * @return {[type]} 7 | */ 8 | /*@ngInject*/ 9 | constructor($scope){ 10 | this.page = this.paging.offset + 1; 11 | $scope.$watch('footer.paging.offset', (newVal) => { 12 | this.offsetChanged(newVal) 13 | }); 14 | } 15 | 16 | /** 17 | * The offset ( page ) changed externally, update the page 18 | * @param {new offset} 19 | */ 20 | offsetChanged(newVal){ 21 | this.page = newVal + 1; 22 | } 23 | 24 | /** 25 | * The pager was invoked 26 | * @param {scope} 27 | */ 28 | onPaged(page){ 29 | this.paging.offset = page - 1; 30 | this.onPage({ 31 | offset: this.paging.offset, 32 | size: this.paging.size 33 | }); 34 | } 35 | 36 | }; 37 | -------------------------------------------------------------------------------- /src/components/footer/FooterDirective.js: -------------------------------------------------------------------------------- 1 | import { FooterController } from './FooterController'; 2 | 3 | export function FooterDirective(){ 4 | return { 5 | restrict: 'E', 6 | controller: FooterController, 7 | controllerAs: 'footer', 8 | scope: true, 9 | bindToController: { 10 | paging: '=', 11 | onPage: '&' 12 | }, 13 | template: 14 | ``, 23 | replace: true 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/body/StyleTranslator.js: -------------------------------------------------------------------------------- 1 | import { TranslateXY } from '../../utils/translate'; 2 | 3 | /** 4 | * This translates the dom position based on the model row index. 5 | * This only exists because Angular's binding process is too slow. 6 | */ 7 | export class StyleTranslator{ 8 | 9 | constructor(height){ 10 | this.height = height; 11 | this.map = new Map(); 12 | } 13 | 14 | /** 15 | * Update the rows 16 | * @param {Array} rows 17 | */ 18 | update(rows){ 19 | let n = 0; 20 | while (n <= this.map.size) { 21 | let dom = this.map.get(n); 22 | let model = rows[n]; 23 | if(dom && model){ 24 | TranslateXY(dom[0].style, 0, model.$$index * this.height); 25 | } 26 | n++; 27 | } 28 | } 29 | 30 | /** 31 | * Register the row 32 | * @param {int} idx 33 | * @param {dom} dom 34 | */ 35 | register(idx, dom){ 36 | this.map.set(idx, dom); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/translate.js: -------------------------------------------------------------------------------- 1 | import { GetVendorPrefixedName } from './vendorPrefixes'; 2 | import { CamelCase } from './utils'; 3 | 4 | // browser detection and prefixing tools 5 | var transform = GetVendorPrefixedName('transform'), 6 | backfaceVisibility = GetVendorPrefixedName('backfaceVisibility'), 7 | hasCSSTransforms = !!GetVendorPrefixedName('transform'), 8 | hasCSS3DTransforms = !!GetVendorPrefixedName('perspective'), 9 | ua = window.navigator.userAgent, 10 | isSafari = (/Safari\//).test(ua) && !(/Chrome\//).test(ua); 11 | 12 | export function TranslateXY(styles, x,y){ 13 | if (hasCSSTransforms) { 14 | if (!isSafari && hasCSS3DTransforms) { 15 | styles[transform] = `translate3d(${x}px, ${y}px, 0)`; 16 | styles[backfaceVisibility] = 'hidden'; 17 | } else { 18 | styles[CamelCase(transform)] = `translate(${x}px, ${y}px)`; 19 | } 20 | } else { 21 | styles.top = y + 'px'; 22 | styles.left = x + 'px'; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | var configuration = { 3 | basePath: './src', 4 | browsers: ['Chrome'], 5 | frameworks: ['angular', 'mocha', 'sinon-chai', 'browserify', 'phantomjs-shim'], 6 | reporters: ['mocha'], 7 | angular: ['mocks'], 8 | files: [ 9 | { pattern: './**/*.spec.js', watched: false } 10 | ], 11 | preprocessors: { 12 | './**/*.js': ['browserify'] 13 | }, 14 | browserify: { 15 | debug: true, 16 | transform: [ 17 | ['babelify', { stage: 0 }] 18 | ] 19 | }, 20 | customLaunchers: { 21 | ChromeTravis: { 22 | base: 'Chrome', 23 | flags: ['--no-sandbox'] 24 | } 25 | }, 26 | client: { 27 | mocha: { 28 | timeout: 20000 29 | } 30 | }, 31 | browserDisconnectTimeout: 20000 32 | }; 33 | 34 | if(process.env.TRAVIS){ 35 | configuration.browsers = ['ChromeTravis']; 36 | configuration.reporters = ['dots']; 37 | } 38 | config.set(configuration); 39 | }; -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | System.config({ 2 | baseURL: "", 3 | defaultJSExtensions: true, 4 | transpiler: "babel", 5 | babelOptions: { 6 | "optional": [ 7 | "runtime" 8 | ] 9 | }, 10 | paths: { 11 | "*": "dist/*.js", 12 | "github:*": "jspm_packages/github/*", 13 | "npm:*": "jspm_packages/npm/*" 14 | }, 15 | 16 | map: { 17 | "angular": "npm:angular@1.4.0", 18 | "babel": "npm:babel-core@5.8.22", 19 | "babel-runtime": "npm:babel-runtime@5.8.20", 20 | "core-js": "npm:core-js@1.1.1", 21 | "github:jspm/nodelibs-process@0.1.1": { 22 | "process": "npm:process@0.10.1" 23 | }, 24 | "npm:angular@1.4.0": { 25 | "process": "github:jspm/nodelibs-process@0.1.1" 26 | }, 27 | "npm:babel-runtime@5.8.20": { 28 | "process": "github:jspm/nodelibs-process@0.1.1" 29 | }, 30 | "npm:core-js@1.1.1": { 31 | "fs": "github:jspm/nodelibs-fs@0.1.2", 32 | "process": "github:jspm/nodelibs-process@0.1.1", 33 | "systemjs-json": "github:systemjs/plugin-json@0.1.0" 34 | } 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /src/utils/polyfill.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Array.prototype.find() 3 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find 4 | */ 5 | (function() { 6 | function polyfill(fnName) { 7 | if (!Array.prototype[fnName]) { 8 | Array.prototype[fnName] = function(predicate /*, thisArg */ ) { 9 | var i, len, test, thisArg = arguments[1]; 10 | 11 | if (typeof predicate !== "function") { 12 | throw new TypeError(); 13 | } 14 | 15 | test = !thisArg ? predicate : function() { 16 | return predicate.apply(thisArg, arguments); 17 | }; 18 | 19 | for (i = 0, len = this.length; i < len; i++) { 20 | if (test(this[i], i, this) === true) { 21 | return fnName === "find" ? this[i] : i; 22 | } 23 | } 24 | 25 | if (fnName !== "find") { 26 | return -1; 27 | } 28 | }; 29 | } 30 | } 31 | 32 | for (var i in { 33 | find: 1, 34 | findIndex: 1 35 | }) { 36 | polyfill(i); 37 | } 38 | }()); 39 | -------------------------------------------------------------------------------- /src/components/menu/dropdown/DropdownDirective.js: -------------------------------------------------------------------------------- 1 | export function DropdownDirective($document, $timeout){ 2 | return { 3 | restrict: 'C', 4 | controller: 'DropdownController', 5 | link: function($scope, $elm, $attrs) { 6 | 7 | function closeDropdown(ev){ 8 | if($elm[0].contains(ev.target) ) { 9 | return; 10 | } 11 | 12 | $timeout(() => { 13 | $scope.open = false; 14 | off(); 15 | }); 16 | }; 17 | 18 | function keydown(ev){ 19 | if (ev.which === 27) { 20 | $timeout(() => { 21 | $scope.open = false; 22 | off(); 23 | }); 24 | } 25 | }; 26 | 27 | function off(){ 28 | $document.unbind('click', closeDropdown); 29 | $document.unbind('keydown', keydown); 30 | }; 31 | 32 | $scope.$watch('open', (newVal) => { 33 | if(newVal){ 34 | $document.bind('click', closeDropdown); 35 | $document.bind('keydown', keydown); 36 | } 37 | }); 38 | 39 | } 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /src/components/body/GroupRowDirective.js: -------------------------------------------------------------------------------- 1 | import { GroupRowController } from './GroupRowController'; 2 | import { TranslateXY } from '../../utils/translate'; 3 | 4 | export function GroupRowDirective(){ 5 | return { 6 | restrict: 'E', 7 | controller: GroupRowController, 8 | controllerAs: 'group', 9 | bindToController: { 10 | row: '=', 11 | onGroupToggle: '&', 12 | expanded: '=', 13 | options: '=' 14 | }, 15 | scope: true, 16 | replace:true, 17 | template: ` 18 |
19 | 21 | 22 | 23 | 24 |
`, 25 | link: function($scope, $elm, $attrs, ctrl){ 26 | // inital render position 27 | TranslateXY($elm[0].style, 0, ctrl.row.$$index * ctrl.options.rowHeight); 28 | 29 | // register w/ the style translator 30 | ctrl.options.internal.styleTranslator.register($scope.$index, $elm); 31 | } 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Swimlane 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 | -------------------------------------------------------------------------------- /src/components/DataTableService.spec.js: -------------------------------------------------------------------------------- 1 | import '../tests/frameworks'; 2 | import {ObjectId} from '../utils/utils'; 3 | import {DataTableService} from './DataTableService'; 4 | 5 | var mock = angular.mock; 6 | 7 | describe('DataTableService', function () { 8 | before(() => { 9 | angular.module('DataTables.Mock', []); 10 | }) 11 | beforeEach(mock.module('DataTables.Mock')); 12 | 13 | beforeEach(mock.module(($provide) => { 14 | $provide.constant('DataTableService', DataTableService); 15 | })); 16 | 17 | beforeEach(mock.inject((DataTableService, $parse) => { 18 | this.DataTableService = DataTableService; 19 | this.$parse = $parse; 20 | })); 21 | 22 | it('should build and save columns', () => { 23 | let id = ObjectId(); 24 | let columnElements = [ 25 | ``, 26 | `{{monkey}} ---- {{$cell}}` 27 | ].map((el) => angular.element(el)[0]); 28 | 29 | this.DataTableService.saveColumns(id, columnElements); 30 | this.DataTableService.buildColumns({}, this.$parse); 31 | 32 | this.DataTableService.columns.should.have.property(id).and.have.length(2); 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /src/components/menu/MenuDirective.js: -------------------------------------------------------------------------------- 1 | export function MenuDirective(){ 2 | return { 3 | restrict: 'E', 4 | controller: 'MenuController', 5 | controllerAs: 'dtm', 6 | scope: { 7 | current: '=', 8 | available: '=' 9 | }, 10 | template: 11 | `` 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /src/components/footer/PagerDirective.js: -------------------------------------------------------------------------------- 1 | import { PagerController } from './PagerController'; 2 | 3 | export function PagerDirective(){ 4 | return { 5 | restrict: 'E', 6 | controller: PagerController, 7 | controllerAs: 'pager', 8 | scope: true, 9 | bindToController: { 10 | page: '=', 11 | size: '=', 12 | count: '=', 13 | onPage: '&' 14 | }, 15 | template: 16 | `
17 | 34 |
`, 35 | replace: true 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /src/utils/vendorPrefixes.js: -------------------------------------------------------------------------------- 1 | import { CamelCase } from './utils'; 2 | 3 | var cache = {}, 4 | testStyle = document.createElement('div').style; 5 | 6 | function getWithPrefix(name) { 7 | for (var i = 0; i < prefixes.length; i++) { 8 | var prefixedName = prefixes[i] + name; 9 | if (prefixedName in testStyle) { 10 | return prefixedName; 11 | } 12 | } 13 | return null; 14 | } 15 | 16 | // Get Prefix 17 | // http://davidwalsh.name/vendor-prefix 18 | var prefix = (function () { 19 | var styles = window.getComputedStyle(document.documentElement, ''), 20 | pre = (Array.prototype.slice 21 | .call(styles) 22 | .join('') 23 | .match(/-(moz|webkit|ms)-/) || (styles.OLink === '' && ['', 'o']) 24 | )[1], 25 | dom = ('WebKit|Moz|MS|O').match(new RegExp('(' + pre + ')', 'i'))[1]; 26 | return { 27 | dom: dom, 28 | lowercase: pre, 29 | css: '-' + pre + '-', 30 | js: pre[0].toUpperCase() + pre.substr(1) 31 | }; 32 | })(); 33 | 34 | /** 35 | * @param {string} property Name of a css property to check for. 36 | * @return {?string} property name supported in the browser, or null if not 37 | * supported. 38 | */ 39 | export function GetVendorPrefixedName(property) { 40 | var name = CamelCase(property) 41 | if(!cache[name]){ 42 | if(testStyle[prefix.css + property] !== undefined) { 43 | cache[name] = prefix.css + property; 44 | } else if(testStyle[property] !== undefined){ 45 | cache[name] = property; 46 | } 47 | } 48 | return cache[name]; 49 | } 50 | -------------------------------------------------------------------------------- /src/components/body/RowController.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import { DeepValueGetter } from '../../utils/utils'; 3 | import { TranslateXY } from '../../utils/translate'; 4 | 5 | export class RowController { 6 | 7 | /** 8 | * Returns the value for a given column 9 | * @param {col} 10 | * @return {value} 11 | */ 12 | getValue(col){ 13 | if(!col.prop) return ''; 14 | return DeepValueGetter(this.row, col.prop); 15 | } 16 | 17 | /** 18 | * Invoked when a cell triggers the tree toggle 19 | * @param {cell} 20 | */ 21 | onTreeToggled(cell){ 22 | this.onTreeToggle({ 23 | cell: cell, 24 | row: this.row 25 | }); 26 | } 27 | 28 | /** 29 | * Calculates the styles for a pin group 30 | * @param {group} 31 | * @return {styles object} 32 | */ 33 | stylesByGroup( group){ 34 | var styles = { 35 | width: this.columnWidths[group] + 'px' 36 | }; 37 | 38 | if(group === 'left'){ 39 | TranslateXY(styles, this.options.internal.offsetX, 0); 40 | } else if(group === 'right'){ 41 | var offset = (((this.columnWidths.total - this.options.internal.innerWidth) - 42 | this.options.internal.offsetX) + this.options.internal.scrollBarWidth) * -1; 43 | TranslateXY(styles, offset, 0); 44 | } 45 | 46 | return styles; 47 | } 48 | 49 | /** 50 | * Invoked when the cell directive's checkbox changed state 51 | */ 52 | onCheckboxChanged(ev){ 53 | this.onCheckboxChange({ 54 | $event: ev, 55 | row: this.row 56 | }); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/dataTable.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import './utils/polyfill'; 3 | 4 | import { ResizableDirective } from './components/header/ResizableDirective'; 5 | import { SortableDirective } from './components/header/SortableDirective'; 6 | import { DataTableDirective } from './components/DataTableDirective'; 7 | import { HeaderDirective } from './components/header/HeaderDirective'; 8 | import { HeaderCellController } from './components/header/HeaderCellController'; 9 | import { HeaderCellDirective } from './components/header/HeaderCellDirective'; 10 | import { BodyDirective } from './components/body/BodyDirective'; 11 | import { ScrollerDirective } from './components/body/ScrollerDirective'; 12 | import { SelectionDirective } from './components/body/SelectionDirective'; 13 | import { RowDirective } from './components/body/RowDirective'; 14 | import { GroupRowDirective } from './components/body/GroupRowDirective'; 15 | import { CellDirective } from './components/body/CellDirective'; 16 | import { FooterDirective } from './components/footer/FooterDirective'; 17 | import { PagerDirective } from './components/footer/PagerDirective'; 18 | 19 | export default angular 20 | .module('data-table', []) 21 | .directive('dtable', DataTableDirective) 22 | .directive('resizable', ResizableDirective) 23 | .directive('sortable', SortableDirective) 24 | .directive('dtHeader', HeaderDirective) 25 | .directive('dtHeaderCell', HeaderCellDirective) 26 | .directive('dtBody', BodyDirective) 27 | .directive('dtScroller', ScrollerDirective) 28 | .directive('dtSeletion', SelectionDirective) 29 | .directive('dtRow', RowDirective) 30 | .directive('dtGroupRow', GroupRowDirective) 31 | .directive('dtCell', CellDirective) 32 | .directive('dtFooter', FooterDirective) 33 | .directive('dtPager', PagerDirective); 34 | -------------------------------------------------------------------------------- /src/components/body/CellController.js: -------------------------------------------------------------------------------- 1 | export class CellController { 2 | 3 | /** 4 | * Calculates the styles for the Cell Directive 5 | * @return {styles object} 6 | */ 7 | styles(){ 8 | return { 9 | width: this.column.width + 'px', 10 | 'min-width': this.column.width + 'px' 11 | }; 12 | } 13 | 14 | /** 15 | * Calculates the css classes for the cell directive 16 | * @param {column} 17 | * @return {class object} 18 | */ 19 | cellClass(){ 20 | var style = { 21 | 'dt-tree-col': this.column.isTreeColumn 22 | }; 23 | 24 | if(this.column.className){ 25 | style[this.column.className] = true; 26 | } 27 | 28 | return style; 29 | } 30 | 31 | /** 32 | * Calculates the tree class styles. 33 | * @return {css classes object} 34 | */ 35 | treeClass(){ 36 | return { 37 | 'dt-tree-toggle': true, 38 | 'icon-right': !this.expanded, 39 | 'icon-down': this.expanded 40 | } 41 | } 42 | 43 | /** 44 | * Invoked when the tree toggle button was clicked. 45 | * @param {event} 46 | */ 47 | onTreeToggled(evt){ 48 | evt.stopPropagation(); 49 | this.expanded = !this.expanded; 50 | this.onTreeToggle({ 51 | cell: { 52 | value: this.value, 53 | column: this.column, 54 | expanded: this.expanded 55 | } 56 | }); 57 | } 58 | 59 | /** 60 | * Invoked when the checkbox was changed 61 | * @param {object} event 62 | */ 63 | onCheckboxChanged(event){ 64 | event.stopPropagation(); 65 | this.onCheckboxChange({ $event: event }); 66 | } 67 | 68 | /** 69 | * Returns the value in its fomatted form 70 | * @return {string} value 71 | */ 72 | getValue(){ 73 | var val = this.column.cellDataGetter ? 74 | this.column.cellDataGetter(this.value) : this.value; 75 | 76 | if(val === undefined || val === null) val = ''; 77 | return val; 78 | } 79 | 80 | }; 81 | -------------------------------------------------------------------------------- /src/utils/throttle.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | 3 | /** 4 | * Debounce helper 5 | * @param {function} 6 | * @param {int} 7 | * @param {boolean} 8 | */ 9 | export function debounce(func, wait, immediate) { 10 | var timeout, args, context, timestamp, result; 11 | return function() { 12 | context = this; 13 | args = arguments; 14 | timestamp = new Date(); 15 | var later = function() { 16 | var last = new Date() - timestamp; 17 | if (last < wait) { 18 | timeout = setTimeout(later, wait - last); 19 | } else { 20 | timeout = null; 21 | if (!immediate) 22 | result = func.apply(context, args); 23 | } 24 | }; 25 | var callNow = immediate && !timeout; 26 | if (!timeout) { 27 | timeout = setTimeout(later, wait); 28 | } 29 | if (callNow) 30 | result = func.apply(context, args); 31 | return result; 32 | }; 33 | }; 34 | 35 | /** 36 | * Throttle helper 37 | * @param {function} 38 | * @param {boolean} 39 | * @param {object} 40 | */ 41 | export function throttle(func, wait, options) { 42 | var context, args, result; 43 | var timeout = null; 44 | var previous = 0; 45 | options || (options = {}); 46 | var later = function() { 47 | previous = options.leading === false ? 0 : new Date(); 48 | timeout = null; 49 | result = func.apply(context, args); 50 | }; 51 | return function() { 52 | var now = new Date(); 53 | if (!previous && options.leading === false) 54 | previous = now; 55 | var remaining = wait - (now - previous); 56 | context = this; 57 | args = arguments; 58 | if (remaining <= 0) { 59 | clearTimeout(timeout); 60 | timeout = null; 61 | previous = now; 62 | result = func.apply(context, args); 63 | } else if (!timeout && options.trailing !== false) { 64 | timeout = setTimeout(later, remaining); 65 | } 66 | return result; 67 | }; 68 | }; 69 | -------------------------------------------------------------------------------- /src/components/header/HeaderCellController.js: -------------------------------------------------------------------------------- 1 | import { NextSortDirection } from '../../utils/utils'; 2 | 3 | export class HeaderCellController{ 4 | /** 5 | * Calculates the styles for the header cell directive 6 | * @return {styles} 7 | */ 8 | styles(){ 9 | return { 10 | width: this.column.width + 'px', 11 | minWidth: this.column.minWidth + 'px', 12 | maxWidth: this.column.maxWidth + 'px', 13 | height: this.column.height + 'px' 14 | }; 15 | } 16 | 17 | /** 18 | * Calculates the css classes for the header cell directive 19 | */ 20 | cellClass(){ 21 | var cls = { 22 | 'sortable': this.column.sortable, 23 | 'resizable': this.column.resizable 24 | }; 25 | 26 | if(this.column.headerClassName){ 27 | cls[this.column.headerClassName] = true; 28 | } 29 | 30 | return cls; 31 | } 32 | 33 | /** 34 | * Toggles the sorting on the column 35 | */ 36 | onSorted(){ 37 | if(this.column.sortable){ 38 | this.column.sort = NextSortDirection(this.sortType, this.column.sort); 39 | 40 | if (this.column.sort === undefined){ 41 | this.column.sortPriority = undefined; 42 | } 43 | 44 | this.onSort({ 45 | column: this.column 46 | }); 47 | } 48 | } 49 | 50 | /** 51 | * Toggles the css class for the sort button 52 | */ 53 | sortClass(){ 54 | return { 55 | 'sort-btn': true, 56 | 'sort-asc icon-down': this.column.sort === 'asc', 57 | 'sort-desc icon-up': this.column.sort === 'desc' 58 | }; 59 | } 60 | 61 | /** 62 | * Updates the column width on resize 63 | * @param {width} 64 | * @param {column} 65 | */ 66 | onResized(width, column){ 67 | this.onResize({ 68 | column: column, 69 | width: width 70 | }); 71 | } 72 | 73 | /** 74 | * Invoked when the header cell directive checkbox was changed 75 | */ 76 | onCheckboxChange(){ 77 | this.onCheckboxChanged(); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/components/body/ScrollerDirective.js: -------------------------------------------------------------------------------- 1 | import { requestAnimFrame } from '../../utils/utils'; 2 | import { StyleTranslator } from './StyleTranslator'; 3 | import { TranslateXY } from '../../utils/translate'; 4 | 5 | export function ScrollerDirective($timeout, $rootScope){ 6 | return { 7 | restrict: 'E', 8 | require:'^dtBody', 9 | transclude: true, 10 | replace: true, 11 | template: `
`, 12 | link: function($scope, $elm, $attrs, ctrl){ 13 | var ticking = false, 14 | lastScrollY = 0, 15 | lastScrollX = 0, 16 | parent = $elm.parent(); 17 | 18 | ctrl.options.internal.styleTranslator = 19 | new StyleTranslator(ctrl.options.rowHeight); 20 | 21 | ctrl.options.internal.setYOffset = function(offsetY){ 22 | parent[0].scrollTop = offsetY; 23 | }; 24 | 25 | function update(){ 26 | ctrl.options.internal.offsetY = lastScrollY; 27 | ctrl.options.internal.offsetX = lastScrollX; 28 | ctrl.updatePage(); 29 | 30 | if(ctrl.options.scrollbarV){ 31 | ctrl.getRows(); 32 | } 33 | 34 | // https://github.com/Swimlane/angular-data-table/pull/74 35 | ctrl.options.$outer.$digest(); 36 | 37 | ticking = false; 38 | }; 39 | 40 | function requestTick() { 41 | if(!ticking) { 42 | requestAnimFrame(update); 43 | ticking = true; 44 | } 45 | }; 46 | 47 | parent.on('scroll', function(ev) { 48 | lastScrollY = this.scrollTop; 49 | lastScrollX = this.scrollLeft; 50 | requestTick(); 51 | }); 52 | 53 | $scope.$on('$destroy', () => { 54 | parent.off('scroll'); 55 | }); 56 | 57 | $scope.scrollerStyles = function(){ 58 | if(ctrl.options.scrollbarV){ 59 | return { 60 | height: ctrl.count * ctrl.options.rowHeight + 'px' 61 | } 62 | } 63 | }; 64 | 65 | } 66 | }; 67 | }; 68 | -------------------------------------------------------------------------------- /src/components/popover/PositionHelper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Position helper for the popover directive. 3 | */ 4 | export function PositionHelper(){ 5 | return { 6 | 7 | calculateVerticalAlignment: function(elDimensions, popoverDimensions, alignment){ 8 | if (alignment === 'top'){ 9 | return elDimensions.top; 10 | } 11 | if (alignment === 'bottom'){ 12 | return elDimensions.top + elDimensions.height - popoverDimensions.height; 13 | } 14 | if (alignment === 'center'){ 15 | return elDimensions.top + elDimensions.height/2 - popoverDimensions.height/2; 16 | } 17 | }, 18 | 19 | calculateVerticalCaret: function(elDimensions, popoverDimensions, caretDimensions, alignment){ 20 | if (alignment === 'top'){ 21 | return elDimensions.height/2 - caretDimensions.height/2 - 1; 22 | } 23 | if (alignment === 'bottom'){ 24 | return popoverDimensions.height - elDimensions.height/2 - caretDimensions.height/2 - 1; 25 | } 26 | if (alignment === 'center'){ 27 | return popoverDimensions.height/2 - caretDimensions.height/2 - 1; 28 | } 29 | }, 30 | 31 | calculateHorizontalCaret: function(elDimensions, popoverDimensions, caretDimensions, alignment){ 32 | if (alignment === 'left'){ 33 | return elDimensions.width/2 - caretDimensions.height/2 - 1; 34 | } 35 | if (alignment === 'right'){ 36 | return popoverDimensions.width - elDimensions.width/2 - caretDimensions.height/2 - 1; 37 | } 38 | if (alignment === 'center'){ 39 | return popoverDimensions.width/2 - caretDimensions.height/2 - 1; 40 | } 41 | }, 42 | 43 | calculateHorizontalAlignment: function(elDimensions, popoverDimensions, alignment){ 44 | if (alignment === 'left'){ 45 | return elDimensions.left; 46 | } 47 | if (alignment === 'right'){ 48 | return elDimensions.left + elDimensions.width - popoverDimensions.width; 49 | } 50 | if (alignment === 'center'){ 51 | return elDimensions.left + elDimensions.width/2 - popoverDimensions.width/2; 52 | } 53 | } 54 | 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/components/header/ResizableDirective.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Resizable directive 3 | * http://stackoverflow.com/questions/18368485/angular-js-resizable-div-directive 4 | * @param {object} 5 | * @param {function} 6 | * @param {function} 7 | */ 8 | export function ResizableDirective($document, $timeout){ 9 | return { 10 | restrict: 'A', 11 | scope:{ 12 | isResizable: '=resizable', 13 | minWidth: '=', 14 | maxWidth: '=', 15 | onResize: '&' 16 | }, 17 | link: function($scope, $element, $attrs){ 18 | if($scope.isResizable){ 19 | $element.addClass('resizable'); 20 | } 21 | 22 | var handle = angular.element(``), 23 | parent = $element.parent(), 24 | prevScreenX; 25 | 26 | handle.on('mousedown', function(event) { 27 | if(!$element[0].classList.contains('resizable')) { 28 | return false; 29 | } 30 | 31 | event.stopPropagation(); 32 | event.preventDefault(); 33 | 34 | $document.on('mousemove', mousemove); 35 | $document.on('mouseup', mouseup); 36 | }); 37 | 38 | function mousemove(event) { 39 | event = event.originalEvent || event; 40 | 41 | var width = parent[0].clientWidth, 42 | movementX = event.movementX || event.mozMovementX || (event.screenX - prevScreenX), 43 | newWidth = width + (movementX || 0); 44 | 45 | prevScreenX = event.screenX; 46 | 47 | if((!$scope.minWidth || newWidth >= $scope.minWidth) && (!$scope.maxWidth || newWidth <= $scope.maxWidth)){ 48 | parent.css({ 49 | width: newWidth + 'px' 50 | }); 51 | } 52 | } 53 | 54 | function mouseup() { 55 | if ($scope.onResize) { 56 | $timeout(function () { 57 | let width = parent[0].clientWidth; 58 | if (width < $scope.minWidth){ 59 | width = $scope.minWidth; 60 | } 61 | $scope.onResize({ width: width }); 62 | }); 63 | } 64 | 65 | $document.unbind('mousemove', mousemove); 66 | $document.unbind('mouseup', mouseup); 67 | } 68 | 69 | $element.append(handle); 70 | } 71 | }; 72 | }; 73 | -------------------------------------------------------------------------------- /src/components/header/SortableDirective.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | 3 | /** 4 | * Sortable Directive 5 | * http://jsfiddle.net/RubaXa/zLq5J/3/ 6 | * https://jsfiddle.net/hrohxze0/6/ 7 | * @param {function} 8 | */ 9 | export function SortableDirective($timeout) { 10 | return { 11 | restrict: 'A', 12 | scope: { 13 | isSortable: '=sortable', 14 | onSortableSort: '&' 15 | }, 16 | link: function($scope, $element, $attrs){ 17 | var rootEl = $element[0], dragEl, nextEl, dropEl; 18 | 19 | function isbefore(a, b) { 20 | if (a.parentNode == b.parentNode) { 21 | for (var cur = a; cur; cur = cur.previousSibling) { 22 | if (cur === b) { 23 | return true; 24 | } 25 | } 26 | } 27 | return false; 28 | }; 29 | 30 | function onDragEnter(e) { 31 | var target = e.target; 32 | if (isbefore(dragEl, target)) { 33 | target.parentNode.insertBefore(dragEl, target); 34 | } else if(target.nextSibling && target.hasAttribute('draggable')) { 35 | target.parentNode.insertBefore(dragEl, target.nextSibling.nextSibling); 36 | } 37 | }; 38 | 39 | function onDragEnd(evt) { 40 | evt.preventDefault(); 41 | 42 | dragEl.classList.remove('dt-clone'); 43 | 44 | $element.off('dragend', onDragEnd); 45 | $element.off('dragenter', onDragEnter); 46 | 47 | if (nextEl !== dragEl.nextSibling) { 48 | $scope.onSortableSort({ 49 | event: evt, 50 | columnId: angular.element(dragEl).attr('data-id') 51 | }); 52 | } 53 | }; 54 | 55 | function onDragStart(evt){ 56 | if(!$scope.isSortable) return false; 57 | evt = evt.originalEvent || evt; 58 | 59 | dragEl = evt.target; 60 | nextEl = dragEl.nextSibling; 61 | dragEl.classList.add('dt-clone'); 62 | 63 | evt.dataTransfer.effectAllowed = 'move'; 64 | evt.dataTransfer.setData('Text', dragEl.textContent); 65 | 66 | $element.on('dragenter', onDragEnter); 67 | $element.on('dragend', onDragEnd); 68 | }; 69 | 70 | $element.on('dragstart', onDragStart); 71 | 72 | $scope.$on('$destroy', () => { 73 | $element.off('dragstart', onDragStart); 74 | }); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/components/DataTableService.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import { ColumnDefaults } from '../defaults'; 3 | import { CamelCase } from '../utils/utils'; 4 | 5 | export let DataTableService = { 6 | 7 | // id: [ column defs ] 8 | columns: {}, 9 | dTables: {}, 10 | 11 | saveColumns(id, columnElms) { 12 | if (columnElms && columnElms.length) { 13 | let columnsArray = [].slice.call(columnElms); 14 | this.dTables[id] = columnsArray; 15 | } 16 | }, 17 | 18 | /** 19 | * Create columns from elements 20 | * @param {array} columnElms 21 | */ 22 | buildColumns(scope, parse) { 23 | //FIXME: Too many nested for loops. O(n3) 24 | 25 | // Iterate through each dTable 26 | angular.forEach(this.dTables, (columnElms, id) => { 27 | this.columns[id] = []; 28 | 29 | // Iterate through each column 30 | angular.forEach(columnElms, (c) => { 31 | let column = {}; 32 | 33 | var visible = true; 34 | // Iterate through each attribute 35 | angular.forEach(c.attributes, (attr) => { 36 | let attrName = CamelCase(attr.name); 37 | 38 | // cuz putting className vs class on 39 | // a element feels weird 40 | switch (attrName) { 41 | case 'class': 42 | column.className = attr.value; 43 | break; 44 | case 'name': 45 | case 'prop': 46 | column[attrName] = attr.value; 47 | break; 48 | case 'headerRenderer': 49 | case 'cellRenderer': 50 | case 'cellDataGetter': 51 | column[attrName] = parse(attr.value); 52 | break; 53 | case 'visible': 54 | visible = parse(attr.value)(scope); 55 | break; 56 | default: 57 | column[attrName] = parse(attr.value)(scope); 58 | break; 59 | } 60 | }); 61 | 62 | let header = c.getElementsByTagName('column-header'); 63 | if(header.length){ 64 | column.headerTemplate = header[0].innerHTML; 65 | c.removeChild(header[0]) 66 | } 67 | 68 | if (c.innerHTML !== '') { 69 | column.template = c.innerHTML; 70 | } 71 | 72 | if (visible) 73 | this.columns[id].push(column); 74 | }); 75 | }); 76 | 77 | this.dTables = {}; 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | We would love for you to contribute to our project and help make it ever better! As a contributor, here are the guidelines we would like you to follow. 3 | 4 | ### Found an Issue? 5 | If you find a bug in the source code or a mistake in the documentation, you can help us by submitting an issue to our GitHub Repository. Including an issue reproduction (via CodePen, JsBin, Plunkr, etc.) is the absolute best way to help the team quickly diagnose the problem. Screenshots are also helpful. 6 | 7 | You can help the team even more and submit a Pull Request with a fix. 8 | 9 | ### Want a Feature? 10 | You can request a new feature by submitting an issue to our GitHub Repository. If you would like to implement a new feature, please submit an issue with a proposal for your work first, to be sure that we can use it. Please consider what kind of change it is: 11 | 12 | - For a Major Feature, first open an issue and outline your proposal so that it can be discussed. This will also allow us to better coordinate our efforts, prevent duplication of work, and help you to craft the change so that it is successfully accepted into the project. 13 | - Small Features can be crafted and directly submitted as a Pull Request. 14 | 15 | ### Issue Etiquette 16 | Before you submit an issue, search the archive, maybe your question was already answered. 17 | 18 | If your issue appears to be a bug, and hasn't been reported, open a new issue. Help us to maximize the effort we can spend fixing issues and adding new features by not reporting duplicate issues. Providing the following information will increase the chances of your issue being dealt with quickly: 19 | 20 | - Overview of the Issue - if an error is being thrown a non-minified stack trace helps 21 | - Angular and angular-data-table Versions - which versions of Angular and angular-data-table are affected 22 | - Motivation for or Use Case - explain what are you trying to do and why the current behavior is a bug for you 23 | - Browsers and Operating System - is this a problem with all browsers? 24 | - Reproduce the Error - provide a live example (using CodePen, JsBin, Plunker, etc.) or a unambiguous set of steps 25 | - Screenshots - Due to the visual nature of angular-data-table, screenshots can help the team triage issues far more quickly than a text descrption. 26 | - Related Issues - has a similar issue been reported before? 27 | - Suggest a Fix - if you can't fix the bug yourself, perhaps you can point to what might be causing the problem (line of code or commit) 28 | -------------------------------------------------------------------------------- /src/components/header/HeaderController.js: -------------------------------------------------------------------------------- 1 | import { TranslateXY } from '../../utils/translate'; 2 | 3 | export class HeaderController { 4 | 5 | /** 6 | * Returns the styles for the header directive. 7 | * @param {object} scope 8 | * @return {object} styles 9 | */ 10 | styles() { 11 | return { 12 | width: this.options.internal.innerWidth + 'px', 13 | height: this.options.headerHeight + 'px' 14 | } 15 | } 16 | 17 | /** 18 | * Returns the inner styles for the header directive 19 | * @param {object} scope 20 | * @return {object} styles 21 | */ 22 | innerStyles(){ 23 | return { 24 | width: this.columnWidths.total + 'px' 25 | }; 26 | } 27 | 28 | /** 29 | * Invoked when a column sort direction has changed 30 | * @param {object} scope 31 | * @param {object} column 32 | */ 33 | onSorted(sortedColumn){ 34 | if (this.options.sortType === 'single') { 35 | // if sort type is single, then only one column can be sorted at once, 36 | // so we set the sort to undefined for the other columns 37 | function unsortColumn(column) { 38 | if (column !== sortedColumn) { 39 | column.sort = undefined; 40 | } 41 | } 42 | 43 | this.columns.left.forEach(unsortColumn); 44 | this.columns.center.forEach(unsortColumn); 45 | this.columns.right.forEach(unsortColumn); 46 | } 47 | 48 | this.onSort({ 49 | column: sortedColumn 50 | }); 51 | } 52 | 53 | /** 54 | * Returns the styles by group for the headers. 55 | * @param {scope} 56 | * @param {group} 57 | * @return {styles object} 58 | */ 59 | stylesByGroup(group){ 60 | var styles = { 61 | width: this.columnWidths[group] + 'px' 62 | }; 63 | 64 | if(group === 'center'){ 65 | TranslateXY(styles, this.options.internal.offsetX * -1, 0); 66 | } else if(group === 'right'){ 67 | var offset = (this.columnWidths.total - this.options.internal.innerWidth) *-1; 68 | TranslateXY(styles, offset, 0); 69 | } 70 | 71 | return styles; 72 | } 73 | 74 | /** 75 | * Invoked when the header cell directive's checkbox has changed. 76 | * @param {scope} 77 | */ 78 | onCheckboxChanged(){ 79 | this.onCheckboxChange(); 80 | } 81 | 82 | /** 83 | * Occurs when a header cell directive triggered a resize 84 | * @param {object} scope 85 | * @param {object} column 86 | * @param {int} width 87 | */ 88 | onResized(column, width){ 89 | this.onResize({ 90 | column: column, 91 | width: width 92 | }); 93 | } 94 | 95 | }; 96 | -------------------------------------------------------------------------------- /src/components/header/HeaderCellDirective.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import { HeaderCellController } from './HeaderCellController'; 3 | 4 | export function HeaderCellDirective($compile){ 5 | return { 6 | restrict: 'E', 7 | controller: HeaderCellController, 8 | controllerAs: 'hcell', 9 | scope: true, 10 | bindToController: { 11 | options: '=', 12 | column: '=', 13 | onCheckboxChange: '&', 14 | onSort: '&', 15 | sortType: '=', 16 | onResize: '&', 17 | selected: '=' 18 | }, 19 | replace: true, 20 | template: 21 | `
27 |
31 | 36 | 38 | 39 | 40 |
41 |
`, 42 | compile: function() { 43 | return { 44 | pre: function($scope, $elm, $attrs, ctrl) { 45 | let label = $elm[0].querySelector('.dt-header-cell-label'), cellScope; 46 | 47 | if(ctrl.column.headerTemplate || ctrl.column.headerRenderer){ 48 | cellScope = ctrl.options.$outer.$new(false); 49 | 50 | // copy some props 51 | cellScope.$header = ctrl.column.name; 52 | cellScope.$index = $scope.$index; 53 | } 54 | 55 | if(ctrl.column.headerTemplate){ 56 | let elm = angular.element(`${ctrl.column.headerTemplate.trim()}`); 57 | angular.element(label).append($compile(elm)(cellScope)); 58 | } else if(ctrl.column.headerRenderer){ 59 | let elm = angular.element(ctrl.column.headerRenderer($elm)); 60 | angular.element(label).append($compile(elm)(cellScope)[0]); 61 | } else { 62 | let val = ctrl.column.name; 63 | if(val === undefined || val === null) val = ''; 64 | label.textContent = val; 65 | } 66 | } 67 | } 68 | } 69 | }; 70 | }; 71 | -------------------------------------------------------------------------------- /demos/cell-templates.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Datagrid - Templates 11 | 12 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /demos/virtual.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Datagrid - Fixed Virtual 11 | 12 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/components/body/BodyDirective.js: -------------------------------------------------------------------------------- 1 | import { BodyController } from './BodyController'; 2 | 3 | export function BodyDirective($timeout){ 4 | return { 5 | restrict: 'E', 6 | controller: BodyController, 7 | controllerAs: 'body', 8 | bindToController: { 9 | columns: '=', 10 | columnWidths: '=', 11 | rows: '=', 12 | options: '=', 13 | selected: '=?', 14 | expanded: '=?', 15 | onPage: '&', 16 | onTreeToggle: '&', 17 | onSelect: '&', 18 | onRowClick: '&', 19 | onRowDblClick: '&' 20 | }, 21 | scope: true, 22 | template: ` 23 |
27 |
28 |
29 |
30 |
31 |
32 | 33 | 41 | 42 | 60 | 61 | 62 |
65 |
66 |
69 |
70 |
` 71 | }; 72 | }; 73 | -------------------------------------------------------------------------------- /src/components/body/CellDirective.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import { CellController } from './CellController'; 3 | 4 | export function CellDirective($rootScope, $compile, $log, $timeout){ 5 | return { 6 | restrict: 'E', 7 | controller: CellController, 8 | scope: true, 9 | controllerAs: 'cell', 10 | bindToController: { 11 | options: '=', 12 | value: '=', 13 | selected: '=', 14 | column: '=', 15 | row: '=', 16 | expanded: '=', 17 | hasChildren: '=', 18 | onTreeToggle: '&', 19 | onCheckboxChange: '&' 20 | }, 21 | template: 22 | `
26 | 31 | 34 | 35 |
`, 36 | replace: true, 37 | compile: function() { 38 | return { 39 | pre: function($scope, $elm, $attrs, ctrl) { 40 | var content = angular.element($elm[0].querySelector('.dt-cell-content')), cellScope; 41 | 42 | // extend the outer scope onto our new cell scope 43 | if(ctrl.column.template || ctrl.column.cellRenderer){ 44 | createCellScope(); 45 | } 46 | 47 | $scope.$watch('cell.row', () => { 48 | if(cellScope){ 49 | cellScope.$destroy(); 50 | 51 | createCellScope(); 52 | 53 | cellScope.$cell = ctrl.value; 54 | cellScope.$row = ctrl.row; 55 | cellScope.$column = ctrl.column; 56 | cellScope.$$watchers = null; 57 | } 58 | 59 | if(ctrl.column.template){ 60 | content.empty(); 61 | var elm = angular.element(`${ctrl.column.template.trim()}`); 62 | content.append($compile(elm)(cellScope)); 63 | } else if(ctrl.column.cellRenderer){ 64 | content.empty(); 65 | var elm = angular.element(ctrl.column.cellRenderer(cellScope, content)); 66 | content.append($compile(elm)(cellScope)); 67 | } else { 68 | content[0].innerHTML = ctrl.getValue(); 69 | } 70 | 71 | }, true); 72 | 73 | function createCellScope(){ 74 | cellScope = ctrl.options.$outer.$new(false); 75 | cellScope.getValue = ctrl.getValue; 76 | } 77 | } 78 | } 79 | } 80 | }; 81 | }; 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-data-table", 3 | "title": "AngularJS Data-Table", 4 | "description": "A feature-rich but lightweight ES6 AngularJS Data Table crafted for large data sets!", 5 | "version": "0.8.1", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:Swimlane/angular-data-table.git", 9 | "github": "http://github.com/swimlane/angular-data-table.git" 10 | }, 11 | "main": "./release/dataTable.js", 12 | "homepage": "http://swimlane.com/", 13 | "author": { 14 | "name": "Swimlane", 15 | "email": "austin@swimlane.com", 16 | "web": "http://swimlane.com/" 17 | }, 18 | "licenses": [ 19 | { 20 | "type": "MIT", 21 | "url": "http://opensource.org/licenses/mit-license.php" 22 | } 23 | ], 24 | "devDependencies": { 25 | "angular": "^1.4.4", 26 | "angular-mocks": "^1.4.3", 27 | "babel-core": "^6.9.0", 28 | "babel-eslint": "^6.0.2", 29 | "babel-plugin-external-helpers": "^6.5.0", 30 | "babel-plugin-transform-es2015-modules-amd": "^6.8.0", 31 | "babel-plugin-transform-es2015-modules-commonjs": "^6.8.0", 32 | "babel-plugin-transform-es2015-modules-umd": "^6.8.0", 33 | "babel-preset-es2015": "^6.9.0", 34 | "babel-preset-stage-0": "^6.5.0", 35 | "babelify": "7.3.0", 36 | "browser-sync": "^2.7.2", 37 | "chai": "^4.2.0", 38 | "chai-as-promised": "^5.3.0", 39 | "del": "^1.2.0", 40 | "gulp": "^3.8.11", 41 | "gulp-babel": "6.1.2", 42 | "gulp-changed": "^1.2.1", 43 | "gulp-header": "^1.2.2", 44 | "gulp-less": "^3.0.3", 45 | "gulp-ng-annotate": "^1.0.0", 46 | "gulp-plumber": "^1.0.1", 47 | "gulp-rename": "^1.2.2", 48 | "gulp-replace": "^0.5.3", 49 | "gulp-uglify": "^1.2.0", 50 | "jspm": "^0.16.1", 51 | "karma": "^0.13.3", 52 | "karma-angular": "0.0.6", 53 | "karma-babel-preprocessor": "^5.2.1", 54 | "karma-browserify": "^4.2.1", 55 | "karma-chrome-launcher": "^0.2.0", 56 | "karma-mocha": "^0.2.0", 57 | "karma-mocha-reporter": "^1.0.3", 58 | "karma-phantomjs-launcher": "^0.2.0", 59 | "karma-phantomjs-shim": "^1.0.0", 60 | "karma-sinon-chai": "^1.0.0", 61 | "mocha": "^2.2.1", 62 | "natives": "^1.1.6", 63 | "phantomjs": "^1.9.17", 64 | "rollup": "^0.7.8", 65 | "run-sequence": "^1.1.0", 66 | "sinon": "^1.15.4", 67 | "sinon-chai": "^2.7.0", 68 | "systemjs-builder": "^0.11.3", 69 | "vinyl-paths": "^1.0.0" 70 | }, 71 | "jspm": { 72 | "dependencies": { 73 | "angular": "npm:angular@^1.4.0" 74 | }, 75 | "devDependencies": { 76 | "babel": "npm:babel-core@^5.8.22", 77 | "babel-runtime": "npm:babel-runtime@^5.6.4", 78 | "core-js": "npm:core-js@^1.1.1" 79 | } 80 | }, 81 | "babel": { 82 | "sourceMaps": false, 83 | "presets": [ 84 | "es2015", 85 | "stage-0" 86 | ], 87 | "moduleIds": false, 88 | "comments": false, 89 | "compact": false 90 | }, 91 | "dependencies": {} 92 | } 93 | -------------------------------------------------------------------------------- /demos/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Datagrid - Basic 11 | 12 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /demos/slow.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Datagrid - Empty 11 | 12 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /demos/empty.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Datagrid - Empty 11 | 12 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /src/components/footer/PagerController.js: -------------------------------------------------------------------------------- 1 | export class PagerController { 2 | 3 | /** 4 | * Creates an instance of the Pager Controller 5 | * @param {object} $scope 6 | */ 7 | /*@ngInject*/ 8 | constructor($scope){ 9 | $scope.$watch('pager.count', (newVal) => { 10 | this.calcTotalPages(this.size, this.count); 11 | this.getPages(this.page || 1); 12 | }); 13 | 14 | $scope.$watch('pager.size', (newVal) => { 15 | this.calcTotalPages(this.size, this.count); 16 | this.getPages(this.page || 1); 17 | }); 18 | 19 | $scope.$watch('pager.page', (newVal) => { 20 | if (newVal !== 0 && newVal <= this.totalPages) { 21 | this.getPages(newVal); 22 | } 23 | }); 24 | 25 | this.getPages(this.page || 1); 26 | } 27 | 28 | /** 29 | * Calculates the total number of pages given the count. 30 | * @return {int} page count 31 | */ 32 | calcTotalPages(size, count) { 33 | var count = size < 1 ? 1 : Math.ceil(count / size); 34 | this.totalPages = Math.max(count || 0, 1); 35 | } 36 | 37 | /** 38 | * Select a page 39 | * @param {int} num 40 | */ 41 | selectPage(num){ 42 | if (num > 0 && num <= this.totalPages) { 43 | this.page = num; 44 | this.onPage({ 45 | page: num 46 | }); 47 | } 48 | } 49 | 50 | /** 51 | * Selects the previous pager 52 | */ 53 | prevPage(){ 54 | if (this.page > 1) { 55 | this.selectPage(--this.page); 56 | } 57 | } 58 | 59 | /** 60 | * Selects the next page 61 | */ 62 | nextPage(){ 63 | this.selectPage(++this.page); 64 | } 65 | 66 | /** 67 | * Determines if the pager can go previous 68 | * @return {boolean} 69 | */ 70 | canPrevious(){ 71 | return this.page > 1; 72 | } 73 | 74 | /** 75 | * Determines if the pager can go forward 76 | * @return {boolean} 77 | */ 78 | canNext(){ 79 | return this.page < this.totalPages; 80 | } 81 | 82 | /** 83 | * Gets the page set given the current page 84 | * @param {int} page 85 | */ 86 | getPages(page) { 87 | var pages = [], 88 | startPage = 1, 89 | endPage = this.totalPages, 90 | maxSize = 5, 91 | isMaxSized = maxSize < this.totalPages; 92 | 93 | if (isMaxSized) { 94 | startPage = ((Math.ceil(page / maxSize) - 1) * maxSize) + 1; 95 | endPage = Math.min(startPage + maxSize - 1, this.totalPages); 96 | } 97 | 98 | for (var number = startPage; number <= endPage; number++) { 99 | pages.push({ 100 | number: number, 101 | text: number, 102 | active: number === page 103 | }); 104 | } 105 | 106 | /* 107 | if (isMaxSized) { 108 | if (startPage > 1) { 109 | pages.unshift({ 110 | number: startPage - 1, 111 | text: '...' 112 | }); 113 | } 114 | 115 | if (endPage < this.totalPages) { 116 | pages.push({ 117 | number: endPage + 1, 118 | text: '...' 119 | }); 120 | } 121 | } 122 | */ 123 | 124 | this.pages = pages; 125 | } 126 | 127 | }; 128 | -------------------------------------------------------------------------------- /demos/scroll.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Datagrid - H/V Scrolling 11 | 12 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/components/body/RowDirective.js: -------------------------------------------------------------------------------- 1 | import { RowController } from './RowController'; 2 | import { TranslateXY } from '../../utils/translate'; 3 | 4 | export function RowDirective(){ 5 | return { 6 | restrict: 'E', 7 | controller: RowController, 8 | controllerAs: 'rowCtrl', 9 | scope: true, 10 | bindToController: { 11 | row: '=', 12 | columns: '=', 13 | columnWidths: '=', 14 | expanded: '=', 15 | selected: '=', 16 | hasChildren: '=', 17 | options: '=', 18 | onCheckboxChange: '&', 19 | onTreeToggle: '&' 20 | }, 21 | link: function($scope, $elm, $attrs, ctrl){ 22 | if(ctrl.row){ 23 | // inital render position 24 | TranslateXY($elm[0].style, 0, ctrl.row.$$index * ctrl.options.rowHeight); 25 | } 26 | 27 | // register w/ the style translator 28 | ctrl.options.internal.styleTranslator.register($scope.$index, $elm); 29 | }, 30 | template: ` 31 |
32 |
35 | 45 | 46 |
47 |
49 | 59 | 60 |
61 |
64 | 74 | 75 |
76 |
`, 77 | replace:true 78 | }; 79 | }; 80 | -------------------------------------------------------------------------------- /demos/greed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Datagrid - Greedy Columns 11 | 12 | 49 | 50 | 51 | 52 | 53 | 54 |
55 | 56 | 57 |

Native flexbox behavior

58 |
59 |
Name
60 |
Gender
61 |
Company
62 |
63 |
64 | 65 | 66 | 67 | 68 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /demos/action-links.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Datagrid - Action Links 11 | 12 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | {{$cell}} 54 | 55 | 56 | 57 | DELETE | 59 | LOG 61 | 62 | 63 | 64 | 65 | 66 | 67 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /demos/grouping.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Datagrid - Grouping 11 | 12 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /demos/expressive.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Datagrid - Expressive 11 | 12 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {{monkey}} ---- {{$cell}} 50 | 51 | 52 | 53 | 54 | 55 | 56 |
57 | {{v}} : {{$row.name}} : {{$cell}} 58 |
59 |
60 |
61 | 62 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /demos/updating.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Datagrid - Basic 11 | 12 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /demos/perf.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Datagrid - Fixed Virtual 11 | 12 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /demos/sort.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Datagrid - Sorting 11 | 12 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /demos/inline-editing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Datagrid - Templates 11 | 12 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |

Double click a name to edit

53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /demos/columnadd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Datagrid - Column Add / Remove 11 | 12 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /src/dataTable.less: -------------------------------------------------------------------------------- 1 | .dt{ 2 | 3 | visibility: hidden; 4 | overflow:hidden; 5 | 6 | &.dt-loaded{ 7 | visibility: visible !important; 8 | } 9 | 10 | *, *:before, *:after { 11 | -moz-box-sizing: border-box; 12 | -webkit-box-sizing: border-box; 13 | box-sizing: border-box; 14 | } 15 | 16 | justify-content: center; 17 | position: relative; 18 | 19 | .dt-clone{ 20 | opacity: .4; 21 | 22 | .dt-resize-handle{ 23 | visibility: hidden; 24 | } 25 | } 26 | 27 | .dt-cell, 28 | .dt-header-cell{ 29 | vertical-align: top; 30 | overflow: hidden; 31 | 32 | //white-space: normal; 33 | white-space: nowrap; 34 | line-height: 1.625; 35 | text-overflow: ellipsis; 36 | 37 | touch-callout: none; 38 | -webkit-user-select: none; 39 | -moz-user-select: none; 40 | -ms-user-select: none; 41 | -o-user-select: none; 42 | user-select: none; 43 | } 44 | 45 | .dt-row, 46 | .dt-header-inner{ 47 | display: -webkit-box; 48 | display: -moz-box; 49 | display: -ms-flexbox; 50 | display: -webkit-flex; 51 | display: flex; 52 | 53 | flex-direction: row; 54 | -webkit-flex-flow: row; 55 | -moz-flex-flow: row; 56 | -ms-flex-flow: row; 57 | -o-flex-flow: row; 58 | flex-flow: row; 59 | flex-flow: row; 60 | } 61 | 62 | .dt-row-left, 63 | .dt-row-right{ 64 | z-index: 9; 65 | } 66 | 67 | .dt-row-left, 68 | .dt-row-center, 69 | .dt-row-right{ 70 | position:relative; 71 | } 72 | 73 | .dt-header{ 74 | overflow:hidden; 75 | 76 | .dt-header-inner{ 77 | white-space: nowrap; 78 | align-items: stretch; 79 | -webkit-align-items: stretch; 80 | } 81 | 82 | .dt-header-cell{ 83 | position:relative; 84 | white-space: nowrap; 85 | display:inline-block; 86 | 87 | &.dt-drag-over{ 88 | background:#EEE; 89 | } 90 | 91 | &.sortable{ 92 | .dt-header-cell-label{ 93 | cursor: pointer; 94 | } 95 | } 96 | 97 | .sort-btn{ 98 | visibility: hidden; 99 | display: inline-block; 100 | 101 | &.sort-desc, 102 | &.sort-asc{ 103 | visibility: visible; 104 | } 105 | } 106 | 107 | .dt-resize-handle{ 108 | display: inline-block; 109 | position: absolute; 110 | right:0; 111 | top:0; 112 | bottom: 0; 113 | width:5px; 114 | padding:0 5px; 115 | visibility: hidden; 116 | cursor: ew-resize; 117 | } 118 | 119 | &.resizable:hover { 120 | .dt-resize-handle{ 121 | visibility: visible; 122 | } 123 | } 124 | 125 | &:last-child { 126 | .dt-resize-handle{ 127 | visibility: hidden !important; 128 | } 129 | } 130 | } 131 | } 132 | 133 | .dt-body{ 134 | overflow:auto; 135 | position:relative; 136 | z-index:10; 137 | 138 | .dt-body-scroller{ 139 | white-space: nowrap; 140 | } 141 | 142 | .dt-group-row{ 143 | outline:none; 144 | } 145 | 146 | .dt-row{ 147 | backface-visibility: hidden; 148 | outline:none; 149 | white-space: nowrap; 150 | 151 | > div { 152 | display: -webkit-box; 153 | display: -moz-box; 154 | display: -ms-flexbox; 155 | display: -webkit-flex; 156 | display: flex; 157 | } 158 | } 159 | 160 | .dt-tree-toggle{ 161 | cursor:pointer; 162 | } 163 | } 164 | 165 | .dt-footer{ 166 | .page-count{ 167 | display:inline-block; 168 | } 169 | 170 | .dt-pager{ 171 | display: inline-block; 172 | float:right; 173 | 174 | .pager, 175 | .pager li { 176 | padding:0; 177 | margin:0; 178 | list-style:none; 179 | } 180 | 181 | .pager{ 182 | li, 183 | li a{ 184 | display:inline-block; 185 | outline:none; 186 | } 187 | } 188 | } 189 | } 190 | 191 | &.fixed { 192 | .dt-body .dt-row, 193 | .dt-body .dt-group-row{ 194 | position: absolute; 195 | } 196 | } 197 | } 198 | 199 | -------------------------------------------------------------------------------- /demos/single-select.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Datagrid - Single Select 11 | 12 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
71 | 77 | 78 |
79 |
Selected item
80 | {{selectedz}} 81 |
82 | 83 |
84 | 85 | 86 | 87 | 88 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /demos/perf-horzscroll.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Datagrid - Fixed Virtual 11 | 12 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /demos/checkboxes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Datagrid - Checkboxes 11 | 12 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
71 | 74 | 75 |
76 |
Selected list
77 | {{selected}} 78 |
79 | 80 |
81 | 82 | 83 | 84 | 85 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /src/defaults.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default Table Options 3 | * @type {object} 4 | */ 5 | export const TableDefaults = { 6 | 7 | // Enable vertical scrollbars 8 | scrollbarV: true, 9 | 10 | // Enable horz scrollbars 11 | // scrollbarH: true, 12 | 13 | // The row height, which is necessary 14 | // to calculate the height for the lazy rendering. 15 | rowHeight: 30, 16 | 17 | // flex 18 | // force 19 | // standard 20 | columnMode: 'standard', 21 | 22 | // Loading message presented when the array is undefined 23 | loadingMessage: 'Loading...', 24 | 25 | // Message to show when array is presented 26 | // but contains no values 27 | emptyMessage: 'No data to display', 28 | 29 | // The minimum header height in pixels. 30 | // pass falsey for no header 31 | headerHeight: 30, 32 | 33 | // The minimum footer height in pixels. 34 | // pass falsey for no footer 35 | footerHeight: 0, 36 | 37 | paging: { 38 | // if external paging is turned on 39 | externalPaging: false, 40 | 41 | // Page size 42 | size: undefined, 43 | 44 | // Total count 45 | count: 0, 46 | 47 | // Page offset 48 | offset: 0, 49 | 50 | // Loading indicator 51 | loadingIndicator: false, 52 | 53 | // template for the footer count text 54 | countText: function(count) { 55 | return `${count} total`; 56 | } 57 | }, 58 | 59 | // if users can select itmes 60 | selectable: false, 61 | 62 | // if users can select mutliple items 63 | multiSelect: false, 64 | 65 | // checkbox selection vs row click 66 | checkboxSelection: false, 67 | 68 | // if you can reorder columns 69 | reorderable: true, 70 | 71 | internal: { 72 | offsetX: 0, 73 | offsetY: 0, 74 | innerWidth: 0, 75 | bodyHeight: 300 76 | }, 77 | 78 | // flag if sorting shuld be handeled externally 79 | externalSorting: false 80 | }; 81 | 82 | /** 83 | * Default Column Options 84 | * @type {object} 85 | */ 86 | export const ColumnDefaults = { 87 | 88 | // pinned to the left 89 | frozenLeft: false, 90 | 91 | // pinned to the right 92 | frozenRight: false, 93 | 94 | // body cell css class name 95 | className: undefined, 96 | 97 | // header cell css class name 98 | headerClassName: undefined, 99 | 100 | // The grow factor relative to other columns. Same as the flex-grow 101 | // API from http://www.w3.org/TR/css3-flexbox/. Basically, 102 | // take any available extra width and distribute it proportionally 103 | // according to all columns' flexGrow values. 104 | flexGrow: 0, 105 | 106 | // Minimum width of the column. 107 | minWidth: 100, 108 | 109 | //Maximum width of the column. 110 | maxWidth: undefined, 111 | 112 | // The width of the column, by default (in pixels). 113 | width: 150, 114 | 115 | // If yes then the column can be resized, otherwise it cannot. 116 | resizable: true, 117 | 118 | // Custom sort comparator 119 | // pass false if you want to server sort 120 | comparator: undefined, 121 | 122 | // If yes then the column can be sorted. 123 | sortable: true, 124 | 125 | // Default sort asecending/descending for the column 126 | sort: undefined, 127 | 128 | // If you want to sort a column by a special property 129 | // See an example in demos/sort.html 130 | sortBy: undefined, 131 | 132 | // The cell renderer that returns content for table column header 133 | headerRenderer: undefined, 134 | 135 | // The cell renderer function(scope, elm) that returns React-renderable content for table cell. 136 | cellRenderer: undefined, 137 | 138 | // The getter function(value) that returns the cell data for the cellRenderer. 139 | // If not provided, the cell data will be collected from row data instead. 140 | cellDataGetter: undefined, 141 | 142 | // Adds +/- button and makes a secondary call to load nested data 143 | isTreeColumn: false, 144 | 145 | // Adds the checkbox selection to the column 146 | isCheckboxColumn: false, 147 | 148 | // Toggles the checkbox column in the header 149 | // for selecting all values given to the grid 150 | headerCheckbox: false, 151 | 152 | // Whether the column can automatically resize to fill space in the table. 153 | canAutoResize: true 154 | 155 | }; 156 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Datagrid 11 | 12 | 50 | 51 | 52 | 53 |

Table Demos

54 | 55 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /src/utils/utils.js: -------------------------------------------------------------------------------- 1 | import { ColumnTotalWidth } from './math'; 2 | 3 | /** 4 | * Shim layer with setTimeout fallback 5 | * http://www.html5rocks.com/en/tutorials/speed/animations/ 6 | */ 7 | export var requestAnimFrame = (function(){ 8 | return window.requestAnimationFrame || 9 | window.webkitRequestAnimationFrame || 10 | window.mozRequestAnimationFrame || 11 | window.oRequestAnimationFrame || 12 | window.msRequestAnimationFrame || 13 | function( callback ){ 14 | window.setTimeout(callback, 1000 / 60); 15 | }; 16 | })(); 17 | 18 | /** 19 | * Creates a unique object id. 20 | */ 21 | export function ObjectId() { 22 | var timestamp = (new Date().getTime() / 1000 | 0).toString(16); 23 | return timestamp + 'xxxxxxxxxxxxxxxx'.replace(/[x]/g, function () { 24 | return (Math.random() * 16 | 0).toString(16); 25 | }).toLowerCase(); 26 | }; 27 | 28 | /** 29 | * Returns the columns by pin. 30 | * @param {array} colsumns 31 | */ 32 | export function ColumnsByPin(cols){ 33 | var ret = { 34 | left: [], 35 | center: [], 36 | right: [] 37 | }; 38 | 39 | for(var i=0, len=cols.length; i < len; i++) { 40 | var c = cols[i]; 41 | if(c.frozenLeft){ 42 | ret.left.push(c) 43 | } else if(c.frozenRight){ 44 | ret.right.push(c); 45 | } else { 46 | ret.center.push(c); 47 | } 48 | } 49 | 50 | return ret; 51 | }; 52 | 53 | /** 54 | * Returns the widths of all group sets of a column 55 | * @param {object} groups 56 | * @param {array} all 57 | */ 58 | export function ColumnGroupWidths(groups, all){ 59 | return { 60 | left: ColumnTotalWidth(groups.left), 61 | center: ColumnTotalWidth(groups.center), 62 | right: ColumnTotalWidth(groups.right), 63 | total: ColumnTotalWidth(all) 64 | }; 65 | } 66 | 67 | /** 68 | * Returns a deep object given a string. zoo['animal.type'] 69 | * @param {object} obj 70 | * @param {string} path 71 | */ 72 | export function DeepValueGetter(obj, path) { 73 | if(!obj || !path) return obj; 74 | 75 | var current = obj, 76 | split = path.split('.'); 77 | 78 | if(split.length){ 79 | for(var i=0, len=split.length; i < len; i++) { 80 | current = current[split[i]]; 81 | } 82 | } 83 | 84 | return current; 85 | }; 86 | 87 | /** 88 | * Converts strings from something to camel case 89 | * http://stackoverflow.com/questions/10425287/convert-dash-separated-string-to-camelcase 90 | * @param {string} str 91 | * @return {string} camel case string 92 | */ 93 | export function CamelCase(str) { 94 | // Replace special characters with a space 95 | str = str.replace(/[^a-zA-Z0-9 ]/g, " "); 96 | // put a space before an uppercase letter 97 | str = str.replace(/([a-z](?=[A-Z]))/g, '$1 '); 98 | // Lower case first character and some other stuff 99 | str = str.replace(/([^a-zA-Z0-9 ])|^[0-9]+/g, '').trim().toLowerCase(); 100 | // uppercase characters preceded by a space or number 101 | str = str.replace(/([ 0-9]+)([a-zA-Z])/g, function(a,b,c) { 102 | return b.trim()+c.toUpperCase(); 103 | }); 104 | return str; 105 | }; 106 | 107 | 108 | /** 109 | * Gets the width of the scrollbar. Nesc for windows 110 | * http://stackoverflow.com/a/13382873/888165 111 | * @return {int} width 112 | */ 113 | export function ScrollbarWidth() { 114 | var outer = document.createElement("div"); 115 | outer.style.visibility = "hidden"; 116 | outer.style.width = "100px"; 117 | outer.style.msOverflowStyle = "scrollbar"; 118 | document.body.appendChild(outer); 119 | 120 | var widthNoScroll = outer.offsetWidth; 121 | outer.style.overflow = "scroll"; 122 | 123 | var inner = document.createElement("div"); 124 | inner.style.width = "100%"; 125 | outer.appendChild(inner); 126 | 127 | var widthWithScroll = inner.offsetWidth; 128 | outer.parentNode.removeChild(outer); 129 | 130 | return widthNoScroll - widthWithScroll; 131 | }; 132 | 133 | export function NextSortDirection(sortType, currentSort) { 134 | if (sortType === 'single') { 135 | if(currentSort === 'asc'){ 136 | return 'desc'; 137 | } else { 138 | return 'asc'; 139 | } 140 | } else { 141 | if(!currentSort){ 142 | return 'asc'; 143 | } else if(currentSort === 'asc'){ 144 | return 'desc'; 145 | } else if(currentSort === 'desc'){ 146 | return undefined; 147 | } 148 | } 149 | }; 150 | -------------------------------------------------------------------------------- /demos/transclude.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Datagrid - Transclude 11 | 12 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {{value}} 50 | 51 | 52 | {{value}}
outerwrapCtrl.bar: {{ outerwrapCtrl.bar }}
foo: {{ foo }}
53 | 54 |
55 |
56 |
57 |
58 |
59 | 60 | 61 | 62 | 63 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /src/components/header/HeaderDirective.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import { HeaderController } from './HeaderController'; 3 | 4 | export function HeaderDirective($timeout){ 5 | return { 6 | restrict: 'E', 7 | controller: HeaderController, 8 | controllerAs: 'header', 9 | scope: true, 10 | bindToController: { 11 | options: '=', 12 | columns: '=', 13 | columnWidths: '=', 14 | onSort: '&', 15 | onResize: '&', 16 | onCheckboxChange: '&' 17 | }, 18 | template: ` 19 |
20 | 21 |
22 |
27 | 36 | 37 |
38 |
42 | 51 | 52 |
53 |
58 | 67 | 68 |
69 |
70 |
`, 71 | replace:true, 72 | link: function($scope, $elm, $attrs, ctrl){ 73 | 74 | $scope.columnsResorted = function(event, columnId){ 75 | var col = findColumnById(columnId), 76 | parent = angular.element(event.currentTarget), 77 | newIdx = -1; 78 | 79 | angular.forEach(parent.children(), (c, i) => { 80 | if (columnId === angular.element(c).attr('data-id')) { 81 | newIdx = i; 82 | } 83 | }); 84 | 85 | $timeout(() => { 86 | angular.forEach(ctrl.columns, (group) => { 87 | var idx = group.indexOf(col); 88 | if(idx > -1){ 89 | 90 | // this is tricky because we want to update the index 91 | // in the orig columns array instead of the grouped one 92 | var curColAtIdx = group[newIdx], 93 | siblingIdx = ctrl.options.columns.indexOf(curColAtIdx), 94 | curIdx = ctrl.options.columns.indexOf(col); 95 | 96 | ctrl.options.columns.splice(curIdx, 1); 97 | ctrl.options.columns.splice(siblingIdx, 0, col); 98 | 99 | return false; 100 | } 101 | }); 102 | 103 | }); 104 | } 105 | 106 | var findColumnById = function(columnId){ 107 | var columns = ctrl.columns.left.concat(ctrl.columns.center).concat(ctrl.columns.right) 108 | return columns.find(function(c){ 109 | return c.$id === columnId; 110 | }) 111 | } 112 | } 113 | }; 114 | }; 115 | -------------------------------------------------------------------------------- /src/components/body/SelectionController.js: -------------------------------------------------------------------------------- 1 | import { KEYS } from '../../utils/keys'; 2 | 3 | export class SelectionController { 4 | 5 | /*@ngInject*/ 6 | constructor($scope){ 7 | this.body = $scope.body; 8 | this.options = $scope.body.options; 9 | this.selected = $scope.body.selected; 10 | } 11 | 12 | /** 13 | * Handler for the keydown on a row 14 | * @param {event} 15 | * @param {index} 16 | * @param {row} 17 | */ 18 | keyDown(ev, index, row){ 19 | if(KEYS[ev.keyCode]){ 20 | ev.preventDefault(); 21 | } 22 | 23 | if (ev.keyCode === KEYS.DOWN) { 24 | var next = ev.target.nextElementSibling; 25 | if(next){ 26 | next.focus(); 27 | } 28 | } else if (ev.keyCode === KEYS.UP) { 29 | var prev = ev.target.previousElementSibling; 30 | if(prev){ 31 | prev.focus(); 32 | } 33 | } else if(ev.keyCode === KEYS.RETURN){ 34 | this.selectRow(index, row); 35 | } 36 | } 37 | 38 | /** 39 | * Handler for the row click event 40 | * @param {object} event 41 | * @param {int} index 42 | * @param {object} row 43 | */ 44 | rowClicked(event, index, row){ 45 | if(!this.options.checkboxSelection){ 46 | // event.preventDefault(); 47 | this.selectRow(event, index, row); 48 | } 49 | 50 | this.body.onRowClick({ row: row }); 51 | } 52 | 53 | /** 54 | * Handler for the row double click event 55 | * @param {object} event 56 | * @param {int} index 57 | * @param {object} row 58 | */ 59 | rowDblClicked(event, index, row){ 60 | if(!this.options.checkboxSelection){ 61 | event.preventDefault(); 62 | this.selectRow(event, index, row); 63 | } 64 | 65 | this.body.onRowDblClick({ row: row }); 66 | } 67 | 68 | /** 69 | * Invoked when a row directive's checkbox was changed. 70 | * @param {index} 71 | * @param {row} 72 | */ 73 | onCheckboxChange(event, index, row){ 74 | this.selectRow(event, index, row); 75 | } 76 | 77 | /** 78 | * Selects a row and places in the selection collection 79 | * @param {index} 80 | * @param {row} 81 | */ 82 | selectRow(event, index, row){ 83 | if(this.options.selectable){ 84 | if(this.options.multiSelect){ 85 | var isCtrlKeyDown = event.ctrlKey || event.metaKey, 86 | isShiftKeyDown = event.shiftKey; 87 | 88 | if(isShiftKeyDown){ 89 | this.selectRowsBetween(index, row); 90 | } else { 91 | var idx = this.selected.indexOf(row); 92 | if(idx > -1){ 93 | this.selected.splice(idx, 1); 94 | } else { 95 | if(this.options.multiSelectOnShift && this.selected.length === 1) { 96 | this.selected.splice(0, 1); 97 | } 98 | this.selected.push(row); 99 | this.body.onSelect({ rows: [ row ] }); 100 | } 101 | } 102 | this.prevIndex = index; 103 | } else { 104 | this.selected = row; 105 | this.body.onSelect({ rows: [ row ] }); 106 | } 107 | } 108 | } 109 | 110 | /** 111 | * Selects the rows between a index. Used for shift click selection. 112 | * @param {index} 113 | */ 114 | selectRowsBetween(index){ 115 | var reverse = index < this.prevIndex, 116 | selecteds = []; 117 | 118 | for(var i=0, len=this.body.rows.length; i < len; i++) { 119 | var row = this.body.rows[i], 120 | greater = i >= this.prevIndex && i <= index, 121 | lesser = i <= this.prevIndex && i >= index; 122 | 123 | var range = {}; 124 | if ( reverse ) { 125 | range = { 126 | start: index, 127 | end: ( this.prevIndex - index ) 128 | } 129 | } else { 130 | range = { 131 | start: this.prevIndex, 132 | end: index + 1 133 | } 134 | } 135 | 136 | if((reverse && lesser) || (!reverse && greater)){ 137 | var idx = this.selected.indexOf(row); 138 | // if reverse shift selection (unselect) and the 139 | // row is already selected, remove it from selected 140 | if ( reverse && idx > -1 ) { 141 | this.selected.splice(idx, 1); 142 | continue; 143 | } 144 | // if in the positive range to be added to `selected`, and 145 | // not already in the selected array, add it 146 | if( i >= range.start && i < range.end ){ 147 | if ( idx === -1 ) { 148 | this.selected.push(row); 149 | selecteds.push(row); 150 | } 151 | } 152 | } 153 | } 154 | 155 | this.body.onSelect({ rows: selecteds }); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /demos/tall.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Datagrid - Tall 11 | 12 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /src/components/DataTableDirective.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import { DataTableController } from './DataTableController'; 3 | import { ScrollbarWidth, ObjectId } from '../utils/utils'; 4 | import { throttle } from '../utils/throttle'; 5 | import { DataTableService } from './DataTableService'; 6 | 7 | export function DataTableDirective($window, $timeout, $parse){ 8 | return { 9 | restrict: 'E', 10 | replace: true, 11 | controller: DataTableController, 12 | scope: true, 13 | bindToController: { 14 | options: '=', 15 | rows: '=', 16 | selected: '=?', 17 | expanded: '=?', 18 | onSelect: '&', 19 | onSort: '&', 20 | onTreeToggle: '&', 21 | onPage: '&', 22 | onRowClick: '&', 23 | onRowDblClick: '&', 24 | onColumnResize: '&' 25 | }, 26 | controllerAs: 'dt', 27 | template: function(element){ 28 | // Gets the column nodes to transposes to column objects 29 | // http://stackoverflow.com/questions/30845397/angular-expressive-directive-design/30847609#30847609 30 | var columns = element[0].getElementsByTagName('column'), 31 | id = ObjectId(); 32 | DataTableService.saveColumns(id, columns); 33 | 34 | return `
35 | 43 | 44 | 55 | 56 | 60 | 61 |
` 62 | }, 63 | compile: function(tElem, tAttrs){ 64 | return { 65 | pre: function($scope, $elm, $attrs, ctrl){ 66 | DataTableService.buildColumns($scope, $parse); 67 | 68 | // Check and see if we had expressive columns 69 | // and if so, lets use those 70 | var id = $elm.attr('data-column-id'), 71 | columns = DataTableService.columns[id]; 72 | if (columns) { 73 | ctrl.options.columns = columns; 74 | } 75 | 76 | ctrl.transposeColumnDefaults(); 77 | ctrl.options.internal.scrollBarWidth = ScrollbarWidth(); 78 | 79 | /** 80 | * Invoked on init of control or when the window is resized; 81 | */ 82 | function resize() { 83 | var rect = $elm[0].getBoundingClientRect(); 84 | 85 | ctrl.options.internal.innerWidth = Math.floor(rect.width); 86 | 87 | if (ctrl.options.scrollbarV) { 88 | var height = rect.height; 89 | 90 | if (ctrl.options.headerHeight) { 91 | height = height - ctrl.options.headerHeight; 92 | } 93 | 94 | if (ctrl.options.footerHeight) { 95 | height = height - ctrl.options.footerHeight; 96 | } 97 | 98 | ctrl.options.internal.bodyHeight = height; 99 | ctrl.calculatePageSize(); 100 | } 101 | 102 | ctrl.adjustColumns(); 103 | }; 104 | 105 | angular.element($window).on('resize', throttle(() => { 106 | $timeout(resize); 107 | })); 108 | 109 | // When an item is hidden for example 110 | // in a tab with display none, the height 111 | // is not calculated correrctly. We need to watch 112 | // the visible attribute and resize if this occurs 113 | var checkVisibility = function() { 114 | var bounds = $elm[0].getBoundingClientRect(), 115 | visible = bounds.width && bounds.height; 116 | if (visible) resize(); 117 | else $timeout(checkVisibility, 100); 118 | }; 119 | checkVisibility(); 120 | 121 | // add a loaded class to avoid flickering 122 | $elm.addClass('dt-loaded'); 123 | 124 | // prevent memory leaks 125 | $scope.$on('$destroy', () => { 126 | angular.element($window).off('resize'); 127 | }); 128 | } 129 | }; 130 | } 131 | }; 132 | }; 133 | -------------------------------------------------------------------------------- /demos/filters.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Datagrid - Basic 11 | 12 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 47 | 48 | 52 | 53 | 54 |
{{$header}}
55 | 61 |
62 | {{$cell}} 63 |
64 | 65 | 66 |
67 | 68 | 69 | 70 | 71 | 106 | 107 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /src/utils/math.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import { ColumnsByPin, ColumnGroupWidths } from './utils'; 3 | 4 | /** 5 | * Calculates the total width of all columns and their groups 6 | * @param {array} columns 7 | * @param {string} property width to get 8 | */ 9 | export function ColumnTotalWidth(columns, prop) { 10 | var totalWidth = 0; 11 | 12 | columns.forEach((c) => { 13 | var has = prop && c[prop]; 14 | totalWidth = totalWidth + (has ? c[prop] : c.width); 15 | }); 16 | 17 | return totalWidth; 18 | } 19 | 20 | /** 21 | * Calculates the Total Flex Grow 22 | * @param {array} 23 | */ 24 | export function GetTotalFlexGrow(columns){ 25 | var totalFlexGrow = 0; 26 | 27 | for (let c of columns) { 28 | totalFlexGrow += c.flexGrow || 0; 29 | } 30 | 31 | return totalFlexGrow; 32 | } 33 | 34 | /** 35 | * Adjusts the column widths. 36 | * Inspired by: https://github.com/facebook/fixed-data-table/blob/master/src/FixedDataTableWidthHelper.js 37 | * @param {array} all columns 38 | * @param {int} width 39 | */ 40 | export function AdjustColumnWidths(allColumns, expectedWidth){ 41 | var columnsWidth = ColumnTotalWidth(allColumns), 42 | totalFlexGrow = GetTotalFlexGrow(allColumns), 43 | colsByGroup = ColumnsByPin(allColumns); 44 | 45 | if (columnsWidth !== expectedWidth){ 46 | ScaleColumns(colsByGroup, expectedWidth, totalFlexGrow); 47 | } 48 | } 49 | 50 | /** 51 | * Resizes columns based on the flexGrow property, while respecting manually set widths 52 | * @param {array} colsByGroup 53 | * @param {int} maxWidth 54 | * @param {int} totalFlexGrow 55 | */ 56 | function ScaleColumns(colsByGroup, maxWidth, totalFlexGrow) { 57 | // calculate total width and flexgrow points for coulumns that can be resized 58 | angular.forEach(colsByGroup, (cols) => { 59 | cols.forEach((column) => { 60 | if (!column.canAutoResize){ 61 | maxWidth -= column.width; 62 | totalFlexGrow -= column.flexGrow; 63 | } else { 64 | column.width = 0; 65 | } 66 | }); 67 | }); 68 | 69 | var hasMinWidth = {} 70 | var remainingWidth = maxWidth; 71 | 72 | // resize columns until no width is left to be distributed 73 | do { 74 | let widthPerFlexPoint = remainingWidth / totalFlexGrow; 75 | remainingWidth = 0; 76 | angular.forEach(colsByGroup, (cols) => { 77 | cols.forEach((column, i) => { 78 | // if the column can be resize and it hasn't reached its minimum width yet 79 | if (column.canAutoResize && !hasMinWidth[i]){ 80 | let newWidth = column.width + column.flexGrow * widthPerFlexPoint; 81 | if (column.minWidth !== undefined && newWidth < column.minWidth){ 82 | remainingWidth += newWidth - column.minWidth; 83 | column.width = column.minWidth; 84 | hasMinWidth[i] = true; 85 | } else { 86 | column.width = newWidth; 87 | } 88 | } 89 | }); 90 | }); 91 | } while (remainingWidth !== 0); 92 | 93 | } 94 | 95 | /** 96 | * Forces the width of the columns to 97 | * distribute equally but overflowing when nesc. 98 | * 99 | * Rules: 100 | * 101 | * - If combined withs are less than the total width of the grid, 102 | * proporation the widths given the min / max / noraml widths to fill the width. 103 | * 104 | * - If the combined widths, exceed the total width of the grid, 105 | * use the standard widths. 106 | * 107 | * - If a column is resized, it should always use that width 108 | * 109 | * - The proporational widths should never fall below min size if specified. 110 | * 111 | * - If the grid starts off small but then becomes greater than the size ( + / - ) 112 | * the width should use the orginial width; not the newly proporatied widths. 113 | * 114 | * @param {array} allColumns 115 | * @param {int} expectedWidth 116 | */ 117 | export function ForceFillColumnWidths(allColumns, expectedWidth, startIdx){ 118 | var contentWidth = 0, 119 | columnsToResize = startIdx > -1 ? 120 | allColumns.slice(startIdx, allColumns.length).filter((c) => { return c.canAutoResize }) : 121 | allColumns.filter((c) => { return c.canAutoResize }); 122 | 123 | allColumns.forEach((c) => { 124 | if(!c.canAutoResize){ 125 | contentWidth += c.width; 126 | } else { 127 | contentWidth += (c.$$oldWidth || c.width); 128 | } 129 | }); 130 | 131 | var remainingWidth = expectedWidth - contentWidth, 132 | additionWidthPerColumn = remainingWidth / columnsToResize.length, 133 | exceedsWindow = contentWidth > expectedWidth; 134 | 135 | columnsToResize.forEach((column) => { 136 | if(exceedsWindow){ 137 | column.width = column.$$oldWidth || column.width; 138 | } else { 139 | if(!column.$$oldWidth){ 140 | column.$$oldWidth = column.width; 141 | } 142 | 143 | var newSize = column.$$oldWidth + additionWidthPerColumn; 144 | if(column.minWith && newSize < column.minWidth){ 145 | column.width = column.minWidth; 146 | } else if(column.maxWidth && newSize > column.maxWidth){ 147 | column.width = column.maxWidth; 148 | } else { 149 | column.width = newSize; 150 | } 151 | } 152 | }); 153 | } 154 | -------------------------------------------------------------------------------- /demos/pins.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Datagrid - Column Pinning 11 | 12 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
62 |

Pinning Options

63 | 69 |
70 | 71 | 72 | 73 | 74 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /demos/tooltip.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Datagrid - Tooltips 11 | 12 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var nPath = require('path'); 2 | var gulp = require('gulp'); 3 | var plumber = require('gulp-plumber'); 4 | var babel = require('gulp-babel'); 5 | var browserSync = require('browser-sync'); 6 | var runSequence = require('run-sequence'); 7 | var less = require('gulp-less'); 8 | var changed = require('gulp-changed'); 9 | var Builder = require('systemjs-builder'); 10 | var vinylPaths = require('vinyl-paths'); 11 | var del = require('del'); 12 | var ngAnnotate = require('gulp-ng-annotate'); 13 | var rollup = require('rollup'); 14 | var rename = require('gulp-rename'); 15 | var uglify = require('gulp-uglify'); 16 | var header = require('gulp-header'); 17 | 18 | var KarmaServer = require('karma').Server; 19 | 20 | var path = { 21 | source: 'src/**/*.js', 22 | less: 'src/**/*.less', 23 | output: 'dist/', 24 | release: 'release/', 25 | outputCss: 'dist/**/*.css' 26 | }; 27 | 28 | var pkg = require('./package.json'); 29 | 30 | var banner = ['/**', 31 | ' * <%= pkg.name %> - <%= pkg.description %>', 32 | ' * @version v<%= pkg.version %>', 33 | ' * @link <%= pkg.homepage %>', 34 | ' * @license <%= pkg.license %>', 35 | ' */', 36 | ''].join('\n'); 37 | 38 | // 39 | // Compile Tasks 40 | // ------------------------------------------------------------ 41 | gulp.task('es6', function () { 42 | return gulp.src(path.source) 43 | .pipe(plumber()) 44 | .pipe(changed(path.output, { extension: '.js' })) 45 | .pipe(babel()) 46 | .pipe(ngAnnotate({ 47 | gulpWarnings: false 48 | })) 49 | .pipe(gulp.dest(path.output)) 50 | .pipe(browserSync.reload({ stream: true })); 51 | }); 52 | 53 | gulp.task('less', function () { 54 | return gulp.src(path.less) 55 | .pipe(changed(path.output, { extension: '.css' })) 56 | .pipe(plumber()) 57 | .pipe(less()) 58 | .pipe(gulp.dest(path.output)) 59 | .pipe(browserSync.reload({ stream: true })); 60 | }); 61 | 62 | gulp.task('clean', function () { 63 | return gulp.src([path.output, path.release]) 64 | .pipe(vinylPaths(del)); 65 | }); 66 | 67 | gulp.task('compile', function (callback) { 68 | return runSequence( 69 | ['less', 'es6'], 70 | callback 71 | ); 72 | }); 73 | 74 | // 75 | // Dev Mode Tasks 76 | // ------------------------------------------------------------ 77 | gulp.task('serve', ['compile'], function (done) { 78 | browserSync({ 79 | open: false, 80 | port: 9000, 81 | server: { 82 | baseDir: ['.'], 83 | middleware: function (req, res, next) { 84 | res.setHeader('Access-Control-Allow-Origin', '*'); 85 | next(); 86 | } 87 | } 88 | }, done); 89 | }); 90 | 91 | gulp.task('watch', ['serve'], function () { 92 | var watcher = gulp.watch([path.source, path.less, '*.html'], ['compile']); 93 | watcher.on('change', function (event) { 94 | console.log('File ' + event.path + ' was ' + event.type + ', running tasks...'); 95 | }); 96 | }); 97 | 98 | // 99 | // Release Tasks 100 | // ------------------------------------------------------------ 101 | 102 | gulp.task('release', function (callback) { 103 | return runSequence( 104 | 'clean', 105 | ['release-less', 'release-build'], 106 | 'release-umd', 107 | 'release-common', 108 | 'release-es6-min', 109 | callback 110 | ); 111 | }); 112 | 113 | gulp.task('release-less', function () { 114 | return gulp.src(['src/themes/*.less', 'src/dataTable.less']) 115 | .pipe(less()) 116 | .pipe(gulp.dest(path.release)); 117 | }); 118 | 119 | gulp.task('release-build', function () { 120 | return rollup.rollup({ 121 | entry: 'src/dataTable.js', 122 | external: ['angular'] 123 | }).then(function (bundle) { 124 | return bundle.write({ 125 | dest: 'release/dataTable.es6.js', 126 | format: 'es6', 127 | moduleName: 'DataTable' 128 | }); 129 | }); 130 | }); 131 | 132 | gulp.task('release-umd', function () { 133 | return gulp.src('release/dataTable.es6.js') 134 | .pipe(babel({ 135 | plugins: [ 136 | "transform-es2015-modules-umd" 137 | ], 138 | moduleId: 'DataTable' 139 | })) 140 | .pipe(ngAnnotate({ 141 | gulpWarnings: false 142 | })) 143 | .pipe(header(banner, { pkg: pkg })) 144 | .pipe(rename('dataTable.js')) 145 | .pipe(gulp.dest("release/")) 146 | }); 147 | 148 | gulp.task('release-common', function () { 149 | return gulp.src('release/dataTable.es6.js') 150 | .pipe(babel({ 151 | plugins: [ 152 | "transform-es2015-modules-commonjs" 153 | ], 154 | moduleId: 'DataTable' 155 | })) 156 | .pipe(ngAnnotate({ 157 | gulpWarnings: false 158 | })) 159 | .pipe(header(banner, { pkg: pkg })) 160 | .pipe(rename('dataTable.cjs.js')) 161 | .pipe(gulp.dest("release/")) 162 | }); 163 | 164 | gulp.task('release-es6-min', function () { 165 | return gulp.src('release/dataTable.es6.js') 166 | .pipe(babel({ 167 | plugins: [ 168 | "transform-es2015-modules-umd" 169 | ], 170 | moduleId: 'DataTable' 171 | })) 172 | .pipe(ngAnnotate({ 173 | gulpWarnings: false 174 | })) 175 | .pipe(uglify()) 176 | .pipe(header(banner, { pkg: pkg })) 177 | .pipe(rename('dataTable.min.js')) 178 | .pipe(gulp.dest("release/")) 179 | }); 180 | 181 | 182 | // 183 | // Test Tasks 184 | // ------------------------------------------------------------ 185 | 186 | gulp.task('test', ['compile'], function (done) { 187 | var server = new KarmaServer({ 188 | configFile: nPath.join(__dirname, 'karma.conf.js'), 189 | singleRun: true 190 | }, function () { 191 | done(); 192 | }); 193 | 194 | server.start(); 195 | }); 196 | 197 | gulp.task('test-watch', ['compile'], function (done) { 198 | var server = new KarmaServer({ 199 | configFile: nPath.join(__dirname, 'karma.conf.js'), 200 | singleRun: false 201 | }, function () { 202 | done(); 203 | }); 204 | 205 | server.start(); 206 | }); 207 | --------------------------------------------------------------------------------