├── .bowerrc ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── Gruntfile.js ├── README.md ├── angular-mesa.d.ts ├── app ├── .buildignore ├── .htaccess ├── 404.html ├── favicon.ico ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ └── glyphicons-halflings-regular.woff ├── images │ └── yeoman.png ├── index.html ├── robots.txt ├── scripts │ ├── app.js │ └── controllers │ │ ├── clickable-rows.js │ │ ├── disabled-columns.js │ │ ├── expandable.js │ │ ├── main.js │ │ ├── max-height.js │ │ ├── perf.js │ │ └── server-side.js └── views │ ├── clickable-rows.html │ ├── disabled-columns.html │ ├── expandable-panel.html │ ├── expandable.html │ ├── main.html │ ├── max-height.html │ ├── perf.html │ └── server-side-interaction.html ├── bower.json ├── dist ├── ap-mesa.css ├── ap-mesa.js ├── ap-mesa.min.css └── ap-mesa.min.js ├── package-lock.json ├── package.json ├── src ├── ap-mesa.css ├── ap-mesa.js ├── controllers │ └── ApMesaController.js ├── directives │ ├── apMesa.js │ ├── apMesaCell.js │ ├── apMesaDummyRows.js │ ├── apMesaExpandable.js │ ├── apMesaPaginationCtrls.js │ ├── apMesaRow.js │ ├── apMesaRows.js │ ├── apMesaSelector.js │ ├── apMesaStatusDisplay.js │ └── apMesaThTitle.js ├── filters │ ├── apMesaRowFilter.js │ └── apMesaRowSorter.js ├── services │ ├── apMesaDebounce.js │ ├── apMesaFilterFunctions.js │ ├── apMesaFormatFunctions.js │ └── apMesaSortFunctions.js └── templates │ ├── apMesa.tpl.html │ ├── apMesaDummyRows.tpl.html │ ├── apMesaPaginationCtrls.tpl.html │ ├── apMesaRows.tpl.html │ └── apMesaStatusDisplay.tpl.html ├── test ├── .jshintrc ├── karma-e2e.conf.js ├── karma-midway.conf.js ├── karma-shared.conf.js ├── karma-unit.conf.js ├── lib │ ├── chai-expect.js │ └── chai-should.js ├── runner.html └── spec │ ├── controllers │ └── table-controller.js │ ├── directives │ ├── ap-mesa-cell.js │ ├── ap-mesa-selector.js │ └── ap-mesa.js │ ├── filters │ ├── table-row-filter.js │ └── table-row-sorter.js │ └── services │ ├── table-filter-functions.js │ ├── table-format-functions.js │ └── table-sort-functions.js └── update-gh-pages.sh /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .tmp 3 | .sass-cache 4 | app/bower_components 5 | /coverage -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": false, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "strict": true, 18 | "trailing": true, 19 | "smarttabs": true, 20 | "globals": { 21 | "angular": false, 22 | "$": false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8.2.1' 4 | before_script: 5 | - 'npm install -g bower grunt-cli' 6 | - 'bower install' 7 | 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ======================================================= 3 | 4 | Note that patch versions might not show up here, but always use the latest patch version under a particular minor version. 5 | As per semver specs, major versions are not backwards-compatible, minor versions are backwards compatible but with new features, 6 | and patch versions are just bug fixes. 7 | 8 | ## `v2.21.0` 9 | 10 | - Added `setFilter` method to api 11 | 12 | ## `v2.20.0` 13 | 14 | - Added `showFiltersRow` transient property, more API methods 15 | 16 | ## `v2.19.0` 17 | 18 | - Added `clearFilterOnColumnHide` and `clearSortOnColumnHide` options 19 | 20 | ## `v2.18.0` 21 | 22 | - Added support for `enabled-columns` input. This allows you to programmatically and dynamically control what columns are currently being shown. 23 | 24 | ## `v2.17.0` 25 | 26 | - Add sort order indicator to angularjs-table (#10) 27 | - Edge number on pagination cases not to show data (#11) 28 | - min-width added to column 29 | 30 | ## `v2.16.0` 31 | 32 | - `clearFilters` function added to table API (#9) 33 | - column is resized improperly (#8) 34 | 35 | ## `v2.15.0` 36 | 37 | - Added ap-mesa-no-data class to wrapper when no data 38 | 39 | ## `v2.14.0` 40 | 41 | - Added ap-mesa-loading-error class to wrapper when error 42 | 43 | ## `v2.13.0` 44 | 45 | - Added `reset` method to the table API. 46 | - Updated classes for loading element 47 | 48 | 49 | ## `v2.12.0` 50 | 51 | - added "table-header-{column.id}" class to all elements 52 | 53 | ## `v2.11.0` 54 | 55 | - Added support for the `on-row-click` attribute. 56 | 57 | ## `v2.10.0` 58 | 59 | - Added server-side interaction support with `options.getData` 60 | - Improved loading/error/no-data message display with the apMesaStatusDisplay directive 61 | - added the `filterPlaceholder` option to column definition objects 62 | 63 | ## `v2.9.0` 64 | 65 | - Added support for `labelTemplate` and `labelTemplateUrl` 66 | 67 | 68 | ## `v2.8.0` 69 | 70 | - Added pagination support 71 | 72 | 73 | ## `v2.7.0` 74 | 75 | - Added typescript definitions 76 | - Added `options` to the filter and format function signatures 77 | 78 | 79 | ## `v2.6.0` 80 | 81 | - Added provider to define global table option defaults 82 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Generated on 2014-03-29 using generator-angular 0.7.1 2 | 'use strict'; 3 | 4 | // # Globbing 5 | // for performance reasons we're only matching one level down: 6 | // 'test/spec/{,*/}*.js' 7 | // use this if you want to recursively match all subfolders: 8 | // 'test/spec/**/*.js' 9 | 10 | module.exports = function (grunt) { 11 | 12 | // Load grunt tasks automatically 13 | require('load-grunt-tasks')(grunt); 14 | 15 | // Time how long tasks take. Can help when optimizing build times 16 | require('time-grunt')(grunt); 17 | 18 | // Define the configuration for all the tasks 19 | grunt.initConfig({ 20 | 21 | // Project settings 22 | yeoman: { 23 | // configurable paths 24 | app: require('./bower.json').appPath || 'app', 25 | src: 'src', 26 | dist: 'dist' 27 | }, 28 | 29 | // Watches files for changes and runs tasks based on the changed files 30 | watch: { 31 | js: { 32 | files: ['<%= yeoman.app %>/scripts/{,*/}*.js', '<%= yeoman.src %>/*.js'], 33 | tasks: ['newer:jshint:all'], 34 | options: { 35 | livereload: true 36 | } 37 | }, 38 | jsTest: { 39 | files: ['test/spec/{,*/}*.js'], 40 | tasks: ['newer:jshint:test', 'karma:unit'] 41 | }, 42 | styles: { 43 | files: ['<%= yeoman.src %>/ap-mesa.css'], 44 | tasks: ['newer:copy:styles', 'autoprefixer'] 45 | }, 46 | gruntfile: { 47 | files: ['Gruntfile.js'] 48 | }, 49 | html2js: { 50 | files: ['<%= yeoman.src %>/**/*.tpl.html'], 51 | tasks: ['html2js:development'] 52 | }, 53 | livereload: { 54 | options: { 55 | livereload: '<%= connect.options.livereload %>' 56 | }, 57 | files: [ 58 | '<%= yeoman.app %>/{,*/}*.html', 59 | '.tmp/styles/{,*/}*.css', 60 | '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' 61 | ] 62 | } 63 | }, 64 | 65 | html2js: { 66 | options: { 67 | 68 | }, 69 | development: { 70 | options: { 71 | base: '.', 72 | module: 'apMesa.templates' 73 | }, 74 | src: ['<%= yeoman.src %>/**/*.tpl.html'], 75 | dest: '.tmp/scripts/templates.js' 76 | }, 77 | dist: { 78 | options: { 79 | base: '.', 80 | module: 'apMesa.templates' 81 | }, 82 | src: ['<%= yeoman.src %>/templates/*.tpl.html'], 83 | dest: '<%= yeoman.dist %>/templates.js' 84 | } 85 | }, 86 | 87 | // The actual grunt server settings 88 | connect: { 89 | options: { 90 | port: 9001, 91 | // Change this to '0.0.0.0' to access the server from outside. 92 | hostname: 'localhost', 93 | livereload: 35730 94 | }, 95 | livereload: { 96 | options: { 97 | base: [ 98 | '.tmp', 99 | '<%= yeoman.app %>', 100 | '.' 101 | ] 102 | } 103 | }, 104 | test: { 105 | options: { 106 | port: 9001, 107 | base: [ 108 | '.tmp', 109 | 'test', 110 | '<%= yeoman.app %>' 111 | ] 112 | } 113 | }, 114 | dist: { 115 | options: { 116 | base: '<%= yeoman.dist %>' 117 | } 118 | }, 119 | webserver: { 120 | options: { 121 | port: 8888, 122 | keepalive: true 123 | } 124 | }, 125 | devserver: { 126 | options: { 127 | port: 8888 128 | } 129 | }, 130 | testserver: { 131 | options: { 132 | port: 9999 133 | } 134 | }, 135 | coverage: { 136 | options: { 137 | base: 'coverage/', 138 | port: 5555, 139 | keepalive: true 140 | } 141 | } 142 | }, 143 | 144 | // Make sure code styles are up to par and there are no obvious mistakes 145 | jshint: { 146 | options: { 147 | jshintrc: '.jshintrc', 148 | reporter: require('jshint-stylish') 149 | }, 150 | all: [ 151 | 'Gruntfile.js', 152 | '<%= yeoman.app %>/scripts/{,*/}*.js' 153 | ], 154 | test: { 155 | options: { 156 | jshintrc: 'test/.jshintrc' 157 | }, 158 | src: ['test/spec/{,*/}*.js'] 159 | } 160 | }, 161 | 162 | // Empties folders to start fresh 163 | clean: { 164 | dist: { 165 | files: [{ 166 | dot: true, 167 | src: [ 168 | '.tmp', 169 | '<%= yeoman.dist %>/*', 170 | '!<%= yeoman.dist %>/.git*' 171 | ] 172 | }] 173 | }, 174 | server: '.tmp', 175 | templates: { 176 | files: [{ 177 | dot: true, 178 | src: [ 179 | '<%= yeoman.dist %>/templates.js', 180 | '<%= yeoman.dist %>/*.tpl.html' 181 | ] 182 | }] 183 | }, 184 | folders: { 185 | files: [{ 186 | src: [ 187 | '<%= yeoman.dist %>/controllers', 188 | '<%= yeoman.dist %>/directives', 189 | '<%= yeoman.dist %>/filters', 190 | '<%= yeoman.dist %>/services', 191 | '<%= yeoman.dist %>/templates' 192 | ] 193 | }] 194 | } 195 | }, 196 | 197 | // Add vendor prefixed styles 198 | autoprefixer: { 199 | options: { 200 | // browsers: ['last 1 version'] 201 | }, 202 | dist: { 203 | files: [{ 204 | expand: true, 205 | cwd: '<%= yeoman.dist %>/', 206 | src: 'ap-mesa.css', 207 | dest: '<%= yeoman.dist %>/' 208 | }] 209 | } 210 | }, 211 | 212 | // Automatically inject Bower components into the app 213 | 'bower-install': { 214 | app: { 215 | html: '<%= yeoman.app %>/index.html', 216 | ignorePath: '<%= yeoman.app %>/' 217 | } 218 | }, 219 | 220 | // Renames files for browser caching purposes 221 | rev: { 222 | dist: { 223 | files: { 224 | src: [ 225 | '<%= yeoman.dist %>/scripts/{,*/}*.js', 226 | '<%= yeoman.dist %>/styles/{,*/}*.css', 227 | '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', 228 | '<%= yeoman.dist %>/styles/fonts/*' 229 | ] 230 | } 231 | } 232 | }, 233 | 234 | // Reads HTML for usemin blocks to enable smart builds that automatically 235 | // concat, minify and revision files. Creates configurations in memory so 236 | // additional tasks can operate on them 237 | useminPrepare: { 238 | html: '<%= yeoman.app %>/index.html', 239 | options: { 240 | dest: '<%= yeoman.dist %>' 241 | } 242 | }, 243 | 244 | // Performs rewrites based on rev and the useminPrepare configuration 245 | usemin: { 246 | html: ['<%= yeoman.dist %>/{,*/}*.html'], 247 | css: ['<%= yeoman.dist %>/styles/{,*/}*.css'], 248 | options: { 249 | assetsDirs: ['<%= yeoman.dist %>'] 250 | } 251 | }, 252 | 253 | // The following *-min tasks produce minified files in the dist folder 254 | imagemin: { 255 | dist: { 256 | files: [{ 257 | expand: true, 258 | cwd: '<%= yeoman.app %>/images', 259 | src: '{,*/}*.{png,jpg,jpeg,gif}', 260 | dest: '<%= yeoman.dist %>/images' 261 | }] 262 | } 263 | }, 264 | svgmin: { 265 | dist: { 266 | files: [{ 267 | expand: true, 268 | cwd: '<%= yeoman.app %>/images', 269 | src: '{,*/}*.svg', 270 | dest: '<%= yeoman.dist %>/images' 271 | }] 272 | } 273 | }, 274 | htmlmin: { 275 | dist: { 276 | options: { 277 | collapseWhitespace: true, 278 | collapseBooleanAttributes: true, 279 | removeCommentsFromCDATA: true, 280 | removeOptionalTags: true 281 | }, 282 | files: [{ 283 | expand: true, 284 | cwd: '<%= yeoman.dist %>', 285 | src: ['*.html', 'views/{,*/}*.html'], 286 | dest: '<%= yeoman.dist %>' 287 | }] 288 | } 289 | }, 290 | 291 | // Allow the use of non-minsafe AngularJS files. Automatically makes it 292 | // minsafe compatible so Uglify does not destroy the ng references 293 | ngmin: { 294 | dist: { 295 | files: [{ 296 | expand: true, 297 | cwd: '<%= yeoman.dist %>/', 298 | src: 'ap-mesa.js', 299 | dest: '<%= yeoman.dist %>/' 300 | }] 301 | } 302 | }, 303 | 304 | // Replace Google CDN references 305 | cdnify: { 306 | dist: { 307 | html: ['<%= yeoman.dist %>/*.html'] 308 | } 309 | }, 310 | 311 | // Copies remaining files to places other tasks can use 312 | copy: { 313 | dist: { 314 | expand: true, 315 | cwd: '<%= yeoman.src %>', 316 | dest: '<%= yeoman.dist %>/', 317 | src: '**/*' 318 | }, 319 | styles: { 320 | expand: true, 321 | cwd: '<%= yeoman.src %>', 322 | dest: '.tmp/styles/', 323 | src: 'ap-mesa.css' 324 | } 325 | }, 326 | 327 | // Run some tasks in parallel to speed up the build process 328 | // concurrent: { 329 | // server: [ 330 | // 'copy:styles' 331 | // ], 332 | // test: [ 333 | // 'copy:styles' 334 | // ], 335 | // dist: [ 336 | // 'copy:styles', 337 | // 'imagemin', 338 | // 'svgmin' 339 | // ] 340 | // }, 341 | 342 | // By default, your `index.html`'s will take care of 343 | // minification. These next options are pre-configured if you do not wish 344 | // to use the Usemin blocks. 345 | cssmin: { 346 | dist: { 347 | files: { 348 | '<%= yeoman.dist %>/ap-mesa.min.css': [ 349 | '<%= yeoman.dist %>/ap-mesa.css' 350 | ] 351 | } 352 | } 353 | }, 354 | uglify: { 355 | dist: { 356 | files: { 357 | '<%= yeoman.dist %>/ap-mesa.min.js': [ 358 | '<%= yeoman.dist %>/ap-mesa.js' 359 | ] 360 | } 361 | } 362 | }, 363 | concat: { 364 | dist: { 365 | options: { 366 | banner: '\'use strict\';\n', 367 | process: function(src, filepath) { 368 | return '// Source: ' + filepath + '\n' + 369 | src.replace(/(^|\n)[ \t]*('use strict'|"use strict");?\s*/g, '$1'); 370 | } 371 | }, 372 | src: ['<%= yeoman.dist %>/**/*.js'], 373 | dest: '<%= yeoman.dist %>/ap-mesa.js' 374 | } 375 | }, 376 | 377 | // Test settings 378 | karma: { 379 | unit: { 380 | configFile: './test/karma-unit.conf.js', 381 | autoWatch: false, 382 | singleRun: true 383 | }, 384 | unit_auto: { 385 | configFile: './test/karma-unit.conf.js' 386 | }, 387 | midway: { 388 | configFile: './test/karma-midway.conf.js', 389 | autoWatch: false, 390 | singleRun: true 391 | }, 392 | midway_auto: { 393 | configFile: './test/karma-midway.conf.js' 394 | }, 395 | e2e: { 396 | configFile: './test/karma-e2e.conf.js', 397 | autoWatch: false, 398 | singleRun: true 399 | }, 400 | e2e_auto: { 401 | configFile: './test/karma-e2e.conf.js' 402 | } 403 | }, 404 | 405 | injector: { 406 | options: { 407 | addRootSlash: false 408 | }, 409 | local_dependencies: { 410 | files: { 411 | '<%= yeoman.app %>/index.html': ['<%= yeoman.src %>/**/*.js'], 412 | } 413 | } 414 | } 415 | }); 416 | 417 | 418 | grunt.registerTask('serve', function (target) { 419 | if (target === 'dist') { 420 | return grunt.task.run(['build', 'connect:dist:keepalive']); 421 | } 422 | 423 | grunt.task.run([ 424 | // clears out .tmp folder 425 | 'clean:server', 426 | 427 | // Copies styles from /styles into .tmp/styles 428 | 'copy:styles', 429 | 430 | // Adds browser prefixes to CSS3 properties 431 | // 'autoprefixer', // not for dev 432 | 433 | // Convert templates to js 434 | 'html2js:development', 435 | 436 | // Serves from .tmp and 437 | 'connect:livereload', 438 | 439 | // Watches for changes, runs tasks based on changes 440 | 'watch' 441 | ]); 442 | }); 443 | 444 | grunt.registerTask('test', ['connect:testserver','karma:unit'/*,'karma:midway', 'karma:e2e'*/]); 445 | grunt.registerTask('test:unit', ['karma:unit']); 446 | grunt.registerTask('test:midway', ['connect:testserver','karma:midway']); 447 | grunt.registerTask('test:e2e', ['connect:testserver', 'karma:e2e']); 448 | 449 | //keeping these around for legacy use 450 | grunt.registerTask('autotest', ['autotest:unit']); 451 | grunt.registerTask('autotest:unit', ['connect:testserver','karma:unit_auto']); 452 | grunt.registerTask('autotest:midway', ['connect:testserver','karma:midway_auto']); 453 | grunt.registerTask('autotest:e2e', ['connect:testserver','karma:e2e_auto']); 454 | 455 | grunt.registerTask('build', [ 456 | // Clears out dist and .tmp folders 457 | 'clean:dist', 458 | // copy css, js 459 | 'copy:dist', 460 | // prefixer 461 | 'autoprefixer:dist', 462 | // minify css 463 | 'cssmin:dist', 464 | // html2js 465 | 'html2js:dist', 466 | // concat files 467 | 'concat:dist', 468 | // remove templates.js 469 | 'clean:templates', 470 | // remove folders from src dir structure 471 | 'clean:folders', 472 | // ngmin js 473 | 'ngmin:dist', 474 | // minify 475 | 'uglify:dist' 476 | ]); 477 | 478 | grunt.registerTask('default', [ 479 | 'newer:jshint', 480 | 'test', 481 | 'build' 482 | ]); 483 | }; 484 | -------------------------------------------------------------------------------- /angular-mesa.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace angular.apMesa { 2 | 3 | type PAGING_STRATEGY = 'PAGINATE' | 'SCROLL' | 'NONE'; 4 | 5 | interface ITableProvider { 6 | setDefaultOptions: (defaults: ITableOptions) => null; 7 | } 8 | 9 | interface ITableService { 10 | setDefaultOptions: (defaults: ITableOptions) => null; 11 | getDefaultOptions: () => ITableOptions; 12 | } 13 | 14 | interface ITableColumn { 15 | // Identifies the column. 16 | id: string; 17 | // The field on each row that this column displays or uses in its format function. 18 | key: string; 19 | // The column heading text (or template or templateUrl). If not present, id is used. 20 | label?: string; 21 | labelTemplate?: string; 22 | labelTemplateUrl?: string; 23 | // If specified, defines row sort function this column uses. See Row Sorting below. 24 | sort?: ITableSorter | string | true; 25 | // or string no undefined If specified, defines row filter function this column uses. See Row Filtering below. 26 | filter?: ITableFilterer | string | true; 27 | // Defines the placeholder text for the filter input, when filter is enabled 28 | filterPlaceholder?: string; 29 | // or string no '' If specified, defines cell format function. See the Cell Formatting section below. 30 | format?: ITableFormatter | string; 31 | // width of column, can include units, e.g. '30px' 32 | width?: string | number; 33 | // If true, column will not be resizable. 34 | lockWidth?: boolean; 35 | // Name of a registered angular filter to use on row[column.key] 36 | ngFilter?: string; 37 | // A string template for the cell contents. 38 | template?: string; 39 | // A template url used with ng-include for cell contents 40 | templateUrl?: string; 41 | // A tooltip for a column header. 42 | title?: string; 43 | // Marks the column as a "selector" column. 44 | selector?: boolean; 45 | // CSS classes to be added to the element 46 | classes?: string; 47 | } 48 | 49 | interface ITableSorter { 50 | (rowA: any, rowB: any, options: ITableOptions, column: ITableColumn): number; 51 | } 52 | 53 | interface ITableFilterer { 54 | (term: string, value: any, formattedValue: string, row: any, column: ITableColumn, options: ITableOptions): boolean; 55 | } 56 | 57 | interface ITableFormatter { 58 | (value: any, row: any, column: ITableColumn, options: ITableOptions): any; 59 | } 60 | 61 | interface IInitialSort { 62 | id: string; 63 | dir: '-' | '+'; 64 | } 65 | 66 | interface IActiveFilter { 67 | column: ITableColumn; 68 | value: string; 69 | } 70 | 71 | interface IActiveSort { 72 | column: ITableColumn; 73 | direction: 'ASC' | 'DESC'; 74 | } 75 | 76 | interface IGetDataResponse { 77 | total: number; 78 | rows: any[]; 79 | } 80 | 81 | interface ITableStorage { 82 | setItem: (key: string, value: string | ITableStorageState) => ng.IPromise | any; 83 | getItem: (key: string) => ng.IPromise | string | ITableStorageState; 84 | removeItem: (key: string) => ng.IPromise | any; 85 | } 86 | 87 | interface ITableApi { 88 | isSelectedAll: () => boolean; 89 | selectAll: () => void; 90 | deselectAll: () => void; 91 | toggleSelectAll: () => void; 92 | setLoading: (isLoading: boolean, triggerDigest?: boolean) => void; 93 | reset: () => void; 94 | clearFilters: () => void; 95 | // Resets rows' sorting order to whatever options.initialSorts. You can also pass an explicit array of IInitialSort objects. 96 | resetRowSort: (sorts?: IInitialSort[]) => void; 97 | hasActiveFilters: () => boolean; 98 | toggleFiltersRow: (showFiltersRow?: boolean) => void; 99 | isFilterRowEnabled: () => boolean; 100 | setFilter: (columnId, value) => void; 101 | } 102 | 103 | interface ITableOptions { 104 | // Number of pixels to pre-render before and after the viewport (default: 10) 105 | rowPadding?: number; 106 | // The classes to use for the sorting icons 107 | sortClasses?: [string, string, string]; 108 | // undefined 109 | storage?: ITableStorage; 110 | // Arbitrary "version" hash used to identify and compare items in storage. 111 | storageHash?: string; 112 | // Used as the key to store and retrieve items from storage, if it is specified. 113 | storageKey?: string; 114 | // If true, will use JSON.stringify to serialize the state of the table before passing to storage.setItem. 115 | // Also means that it will use JSON.parse to deserialize state in storage.getItem 116 | stringifyStorage?: boolean; 117 | // Array of objects defining an initial sort order. Each object must have id and dir, can be "+" for ascending, "-" for descending. 118 | initialSorts?: IInitialSort[]; 119 | // String to show when data is loading 120 | loadingText?: string; 121 | // String to show when no rows are visible 122 | noRowsText?: string; 123 | // Path to template for td when loading 124 | loadingTemplateUrl?: string; 125 | // undefined Promise object for table data loading. Used to resolve loading state when data is available. 126 | loadingPromise?: ng.IPromise; 127 | // undefined Path to template for td when there is an error loading table data. 128 | loadingErrorTemplateUrl?: string; 129 | // 'error loading results' string to show when loading fails 130 | loadingErrorText?: string; 131 | // undefined Path to template for td when there are no rows to show. 132 | noRowsTemplateUrl?: string; 133 | // 100 Wait time when debouncing the scroll event. Used when updating rows. Milliseconds. 134 | scrollDebounce?: number; 135 | // 1 The background-size css attribute of the placeholder rows is set to bgSizeMultiplier * rowHeight. 136 | bgSizeMultiplier?: number; 137 | // 40 When there are no rows to calculate the height, this number is used as the fallback 138 | defaultRowHeight?: number; 139 | // 300 The pixel height for the body of the table. Note that unless fixedHeight is set to true, this will behave as a max-height. 140 | bodyHeight?: number; 141 | // false If true, the table will fill the calculated height of the parent element. Note that this overrides bodyHeight. The table will listen for 'apMesa:resize' events from the rootScope to recalculate the height. 142 | fillHeight?: boolean; 143 | // false If true, the table body will always have a height of bodyHeight, regardless of whether the rows fill up the vertical space. 144 | fixedHeight?: boolean; 145 | // Provides access to select table controller methods, including selectAll, deselectAll, isSelectedAll, setLoading, etc. 146 | onRegisterApi?: (api: ITableApi) => any; 147 | // {} Customize the way to get column value. If not specified, get columen value by row[column.key] 148 | getter?: (key: string, row: any) => string; 149 | // undefined A template reference to be used for the expandable row feature. See Expandable Rows below. 150 | expandableTemplateUrl?: string; 151 | expandableTemplate?: string; 152 | // SCROLL 153 | pagingStrategy?: PAGING_STRATEGY; 154 | rowsPerPage?: number; 155 | rowsPerPageChoices?: number[]; 156 | rowsPerPageMessage?: string; 157 | showRowsPerPageCtrls?: boolean; 158 | maxPageLinks?: number; 159 | // Async server-side interaction support 160 | getData?: ( 161 | offset: number, 162 | limit: number, 163 | activeFilters: IActiveFilter[], 164 | activeSorts: IActiveSort[] 165 | ) => ng.IPromise; 166 | // If true, will show a number indicating stacked sort priority of each column being sorted. 167 | showSortPriority?: boolean; 168 | // If true, a column's filter state will be removed when that column is hidden. 169 | clearFilterOnColumnHide?: boolean; 170 | // If true, a column's sort state will be removed when that column is hidden. 171 | clearSortOnColumnHide?: boolean; 172 | } 173 | interface IRowScope extends ng.IScope { 174 | toggleRowExpand: Function; 175 | rowIsExpanded: boolean; 176 | refreshExpandedHeight: Function; 177 | row: any; 178 | options: ITableOptions; 179 | } 180 | interface ICellScope extends ng.IScope { 181 | toggleRowExpand: Function; 182 | rowIsExpanded: boolean; 183 | refreshExpandedHeight: Function; 184 | row: any; 185 | column: ITableColumn; 186 | options: ITableOptions; 187 | } 188 | interface ITableStorageState { 189 | sortOrder: IInitialSort[]; 190 | searchTerms: { [columnId: string]: string }; 191 | enabledColumns: string[]; 192 | options: { 193 | rowLimit: number; 194 | pagingStrategy: PAGING_STRATEGY; 195 | storageHash?: string; 196 | }; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /app/.buildignore: -------------------------------------------------------------------------------- 1 | *.coffee -------------------------------------------------------------------------------- /app/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Not Found :( 6 | 141 | 142 | 143 |
144 |

Not found :(

145 |

Sorry, but the page you were trying to view does not exist.

146 |

It looks like this was the result of either:

147 |
    148 |
  • a mistyped address
  • 149 |
  • an out-of-date link
  • 150 |
151 | 154 | 155 |
156 | 157 | 158 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyperlitch/angularjs-table/23ff0603afd782df2e09c4b924f8fa12cbb82052/app/favicon.ico -------------------------------------------------------------------------------- /app/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyperlitch/angularjs-table/23ff0603afd782df2e09c4b924f8fa12cbb82052/app/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /app/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyperlitch/angularjs-table/23ff0603afd782df2e09c4b924f8fa12cbb82052/app/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /app/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyperlitch/angularjs-table/23ff0603afd782df2e09c4b924f8fa12cbb82052/app/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /app/images/yeoman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyperlitch/angularjs-table/23ff0603afd782df2e09c4b924f8fa12cbb82052/app/images/yeoman.png -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | andyperlitch: angularjs-table 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 60 | 61 |
62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /app/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /app/scripts/controllers/clickable-rows.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('apMesa.ghPage') 4 | .controller('ClickableRowsCtrl', function($scope, $q, phoneData, $templateCache) { 5 | 6 | $scope.my_table_options = { 7 | expandableTemplateUrl: 'views/expandable-panel.html', 8 | bodyHeight: 600, 9 | rowPadding: 600 10 | }; 11 | $scope.my_table_columns = [ 12 | { 13 | id: 'name', 14 | key: 'DeviceName', 15 | label: 'Phone', 16 | sort: 'string', 17 | filter: 'like', 18 | template: '' + 19 | '' + 20 | '{{ row.DeviceName }}' 21 | }, 22 | { 23 | id: 'brand', 24 | key: 'Brand', 25 | sort: 'string', 26 | label: 'Brand' 27 | }, 28 | { 29 | id: 'tech', 30 | key: 'technology', 31 | sort: 'string', 32 | label: 'Tech' 33 | } 34 | ]; 35 | $scope.phoneData = phoneData; 36 | 37 | }); 38 | -------------------------------------------------------------------------------- /app/scripts/controllers/disabled-columns.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('apMesa.ghPage') 4 | .controller('DisabledColumnsCtrl', function($scope, $q, phoneData, $templateCache) { 5 | 6 | this.my_table_options = { 7 | bodyHeight: 600, 8 | rowPadding: 600, 9 | storage: localStorage, 10 | storageKey: 'example' 11 | }; 12 | this.my_table_options_paginated = angular.extend({ pagingStrategy: 'PAGINATE' }, this.my_table_options); 13 | this.my_selected_rows = []; 14 | this.my_table_columns = [ 15 | { 16 | id: 'name', 17 | key: 'DeviceName', 18 | label: 'Phone', 19 | sort: 'string', 20 | filter: 'like', 21 | template: '' + 22 | '' + 23 | '' + 24 | '{{ row.DeviceName }}' + 25 | '' 26 | }, 27 | { 28 | id: 'brand', 29 | key: 'Brand', 30 | sort: 'string', 31 | label: 'Brand' 32 | }, 33 | { 34 | id: 'edge', 35 | key: 'edge', 36 | label: 'Edge', 37 | sort: 'string', 38 | filter: 'like' 39 | }, 40 | { 41 | id: 'tech', 42 | key: 'technology', 43 | sort: 'string', 44 | label: 'Tech' 45 | } 46 | ]; 47 | this.my_enabled_columns = this.my_table_columns.map(function(c) { return c.id; }); 48 | this.phoneData = phoneData; 49 | var _this = this; 50 | 51 | this.toggleColumn = function(id) { 52 | var curIndexOfColumnId = _this.my_enabled_columns.indexOf(id); 53 | if (curIndexOfColumnId > -1) { 54 | _this.my_enabled_columns.splice(curIndexOfColumnId, 1); 55 | } else { 56 | _this.my_enabled_columns.push(id); 57 | var enabled = _this.my_enabled_columns; 58 | // Do this to preserve original order for this demo. Do whatever you want in your own use-case 59 | _this.my_enabled_columns = _this.my_table_columns 60 | .filter(function(c) { return enabled.indexOf(c.id) > -1; }) 61 | .map(function(c) { return c.id; }); 62 | } 63 | }; 64 | 65 | }); 66 | -------------------------------------------------------------------------------- /app/scripts/controllers/expandable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('apMesa.ghPage') 4 | .controller('ExpandableCtrl', function($scope, $q, phoneData, $templateCache) { 5 | 6 | $scope.my_table_options = { 7 | expandableTemplateUrl: 'views/expandable-panel.html', 8 | bodyHeight: 600, 9 | rowPadding: 600 10 | }; 11 | $scope.my_table_options_paginated = angular.extend({ pagingStrategy: 'PAGINATE' }, $scope.my_table_options); 12 | $scope.my_selected_rows = []; 13 | $scope.my_table_columns = [ 14 | { 15 | id: 'name', 16 | key: 'DeviceName', 17 | label: 'Phone', 18 | sort: 'string', 19 | filter: 'like', 20 | template: '' + 21 | '' + 22 | '' + 23 | '{{ row.DeviceName }}' + 24 | '' 25 | }, 26 | { 27 | id: 'brand', 28 | key: 'Brand', 29 | sort: 'string', 30 | label: 'Brand' 31 | }, 32 | { 33 | id: 'tech', 34 | key: 'technology', 35 | sort: 'string', 36 | label: 'Tech' 37 | } 38 | ]; 39 | $scope.phoneData = phoneData; 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /app/scripts/controllers/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('apMesa.ghPage') 4 | 5 | // angular filter, to be used with the "ngFilter" option in column definitions below 6 | .filter('commaGroups', function() { 7 | 8 | // Converts a number like 123456789 to string with appropriate commas: "123,456,789" 9 | function commaGroups(value) { 10 | if (typeof value === 'undefined') { 11 | return '-'; 12 | } 13 | var parts = value.toString().split('.'); 14 | parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); 15 | return parts.join('.'); 16 | } 17 | return commaGroups; 18 | }) 19 | .controller('MainCtrl', function ($scope, $templateCache, $q) { 20 | 21 | // Format functions, used with the "format" option in column definitions below 22 | // converts number in inches to display string, eg. 69 => 5'9" 23 | function inches2feet(inches, model){ 24 | var feet = Math.floor(inches/12); 25 | inches = inches % 12; 26 | return feet + '\'' + inches + '"'; 27 | } 28 | // Custom column filtering function: 29 | // If the user types "tall", only people who 30 | // are taller than 70 inches will be displayed. 31 | function feet_filter(term, value, formatted, model) { 32 | if (term === 'tall') { return value > 70; } 33 | if (term === 'short') { return value < 69; } 34 | return true; 35 | } 36 | feet_filter.title = 'Type in "short" or "tall"'; 37 | 38 | // Generates `num` random rows 39 | function genRows(num){ 40 | var retVal = []; 41 | for (var i=0; i < num; i++) { 42 | retVal.push(genRow(i)); 43 | } 44 | return retVal; 45 | } 46 | 47 | // Generates a row with random data 48 | function genRow(id){ 49 | 50 | var fnames = ['joe','fred','frank','jim','mike','gary','aziz']; 51 | var lnames = ['sterling','smith','erickson','burke','ansari']; 52 | var seed = Math.random(); 53 | var seed2 = Math.random(); 54 | var first_name = fnames[ Math.round( seed * (fnames.length -1) ) ]; 55 | var last_name = lnames[ Math.round( seed * (lnames.length -1) ) ]; 56 | 57 | return { 58 | id: id, 59 | selected: false, 60 | first_name: first_name, 61 | last_name: last_name, 62 | age: Math.ceil(seed * 75) + 15, 63 | height: Math.round( seed2 * 36 ) + 48, 64 | weight: Math.round( seed2 * 130 ) + 90, 65 | likes: Math.round(seed2 * seed * 1000000) 66 | }; 67 | } 68 | 69 | // Simulate location of template file 70 | $templateCache.put('path/to/example/template.html', '{{row[column.key]}}'); 71 | $templateCache.put('path/to/example/labelTemplate.html', 'Weight '); 72 | 73 | // Table column definition objects 74 | $scope.my_table_columns = [ 75 | { id: 'selected', key: 'id', label: '', width: 30, lockWidth: true, selector: true }, 76 | //{ id: 'selected', key: 'id', label: '', width: 30, lockWidth: true, selector: true, selectObject: true }, 77 | { id: 'ID', key: 'id', label: 'id', sort: 'number', filter: 'number' }, 78 | { id: 'first_name', key: 'first_name', label: 'First Name', sort: 'string', filter: 'like', template: '{{row[column.key]}}' }, 79 | { id: 'last_name', key: 'last_name', label: 'Last Name', sort: 'string', filter: 'like', templateUrl: 'path/to/example/template.html' }, 80 | { id: 'age', key: 'age', label: 'Age', sort: 'number', filter: 'number' }, 81 | { id: 'likes', key: 'likes', labelTemplate: '', ngFilter: 'commaGroups' }, 82 | { id: 'height', key: 'height', label: 'Height', format: inches2feet, filter: feet_filter, sort: 'number' }, 83 | { id: 'weight', key: 'weight', labelTemplateUrl: 'path/to/example/labelTemplate.html', filter: 'number', sort: 'number' } 84 | ]; 85 | 86 | // Table data 87 | $scope.my_table_data = []; 88 | 89 | 90 | // Selected rows 91 | $scope.my_selected_rows = []; 92 | 93 | // table options 94 | var dataDfd = $q.defer(); 95 | $scope.my_table_options = { 96 | rowLimit: 10, 97 | storage: localStorage, 98 | storageKey: 'gh-page-table', 99 | storageHash: 'a9s8df9a8s7df98as7df', 100 | // getter: function(key, row) { 101 | // return row[key]; 102 | // }, 103 | loading: true, 104 | loadingPromise: dataDfd.promise 105 | }; 106 | $scope.my_table_options_paginate = angular.extend({}, $scope.my_table_options, { 107 | pagingStrategy: 'PAGINATE', 108 | rowsPerPage: 8 109 | }); 110 | 111 | // kick off interval that updates the dataset 112 | setInterval(function() { 113 | $scope.my_table_data = genRows(1000); 114 | dataDfd.resolve(); 115 | $scope.$apply(); 116 | }, 1000); 117 | 118 | }); 119 | -------------------------------------------------------------------------------- /app/scripts/controllers/max-height.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('apMesa.ghPage') 4 | 5 | // angular filter, to be used with the "ngFilter" option in column definitions below 6 | .filter('commaGroups', function() { 7 | 8 | // Converts a number like 123456789 to string with appropriate commas: "123,456,789" 9 | function commaGroups(value) { 10 | if (typeof value === 'undefined') { 11 | return '-'; 12 | } 13 | var parts = value.toString().split('.'); 14 | parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); 15 | return parts.join('.'); 16 | } 17 | return commaGroups; 18 | }) 19 | .controller('MaxHeightCtrl', function ($scope, $templateCache, $q) { 20 | 21 | // Format functions, used with the "format" option in column definitions below 22 | // converts number in inches to display string, eg. 69 => 5'9" 23 | function inches2feet(inches, model){ 24 | var feet = Math.floor(inches/12); 25 | inches = inches % 12; 26 | return feet + '\'' + inches + '"'; 27 | } 28 | // Custom column filtering function: 29 | // If the user types "tall", only people who 30 | // are taller than 70 inches will be displayed. 31 | function feet_filter(term, value, formatted, model) { 32 | if (term === 'tall') { return value > 70; } 33 | if (term === 'short') { return value < 69; } 34 | return true; 35 | } 36 | feet_filter.title = 'Type in "short" or "tall"'; 37 | 38 | // Generates `num` random rows 39 | function genRows(num){ 40 | var retVal = []; 41 | for (var i=0; i < num; i++) { 42 | retVal.push(genRow(i)); 43 | } 44 | return retVal; 45 | } 46 | 47 | // Generates a row with random data 48 | function genRow(id){ 49 | 50 | var fnames = ['joe','fred','frank','jim','mike','gary','aziz']; 51 | var lnames = ['sterling','smith','erickson','burke','ansari']; 52 | var seed = Math.random(); 53 | var seed2 = Math.random(); 54 | var first_name = fnames[ Math.round( seed * (fnames.length -1) ) ]; 55 | var last_name = lnames[ Math.round( seed * (lnames.length -1) ) ]; 56 | 57 | return { 58 | id: id, 59 | selected: false, 60 | first_name: first_name, 61 | last_name: last_name, 62 | age: Math.ceil(seed * 75) + 15, 63 | height: Math.round( seed2 * 36 ) + 48, 64 | weight: Math.round( seed2 * 130 ) + 90, 65 | likes: Math.round(seed2 * seed * 1000000) 66 | }; 67 | } 68 | 69 | // Simulate location of template file 70 | $templateCache.put('path/to/example/template.html', '{{row[column.key]}}'); 71 | 72 | // Table column definition objects 73 | $scope.my_table_columns = [ 74 | { id: 'selected', key: 'id', label: '', width: 30, lockWidth: true, selector: true }, 75 | { id: 'ID', key: 'id', label: 'ID', sort: 'number', filter: 'number' }, 76 | { id: 'first_name', key: 'first_name', label: 'First Name', sort: 'string', filter: 'like', template: '{{row[column.key]}}' }, 77 | { id: 'last_name', key: 'last_name', label: 'Last Name', sort: 'string', filter: 'like', templateUrl: 'path/to/example/template.html' }, 78 | { id: 'age', key: 'age', label: 'Age', sort: 'number', filter: 'number' }, 79 | { id: 'likes', key: 'likes', label: 'likes', ngFilter: 'commaGroups' }, 80 | { id: 'height', key: 'height', label: 'Height', format: inches2feet, filter: feet_filter, sort: 'number' }, 81 | { id: 'weight', key: 'weight', label: 'Weight', filter: 'number', sort: 'number' } 82 | ]; 83 | 84 | // Table data 85 | $scope.my_table_data = []; 86 | 87 | 88 | // Selected rows 89 | $scope.my_selected_rows = []; 90 | 91 | // table options 92 | var apiDfd = $q.defer(); 93 | $scope.my_table_options = { 94 | rowLimit: 10, 95 | storage: localStorage, 96 | storageKey: 'gh-page-table', 97 | loading: true, 98 | onRegisterApi: function(api) { 99 | $scope.api = api; 100 | apiDfd.resolve(); 101 | } 102 | }; 103 | 104 | // kick off interval that updates the dataset 105 | var id = 1; 106 | setInterval(function() { 107 | if (id < 1000) { 108 | $scope.my_table_data.push(genRow(id++)); 109 | $scope.$apply(); 110 | apiDfd.promise.then(function() { 111 | $scope.api.setLoading(false); 112 | }); 113 | } 114 | }, 1000); 115 | 116 | }); 117 | -------------------------------------------------------------------------------- /app/scripts/controllers/perf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('apMesa.ghPage') 4 | .controller('PerfCtrl', function ($scope) { 5 | 6 | // Format functions 7 | function inches2feet(inches, model){ 8 | var feet = Math.floor(inches/12); 9 | inches = inches % 12; 10 | return feet + '\'' + inches + '"'; 11 | } 12 | function feet_filter(term, value, formatted, model) { 13 | if (term === 'tall') { return value > 70; } 14 | if (term === 'short') { return value < 69; } 15 | return true; 16 | } 17 | feet_filter.title = 'Type in "short" or "tall"'; 18 | 19 | // Random data generator 20 | function genRows(num){ 21 | var retVal = []; 22 | for (var i=0; i < num; i++) { 23 | retVal.push(genRow(i)); 24 | } 25 | return retVal; 26 | } 27 | function genRow(id){ 28 | 29 | var fnames = ['joe','fred','frank','jim','mike','gary','aziz']; 30 | var lnames = ['sterling','smith','erickson','burke','ansari']; 31 | var seed = Math.random(); 32 | var seed2 = Math.random(); 33 | var first_name = fnames[ Math.round( seed * (fnames.length -1) ) ]; 34 | var last_name = lnames[ Math.round( seed * (lnames.length -1) ) ]; 35 | 36 | return { 37 | id: id, 38 | selected: false, 39 | first_name: first_name, 40 | last_name: last_name, 41 | age: Math.ceil(seed * 75) + 15, 42 | height: Math.round( seed2 * 36 ) + 48, 43 | weight: Math.round( seed2 * 130 ) + 90 44 | }; 45 | } 46 | 47 | // Table columns 48 | $scope.my_table_columns = [ 49 | { id: 'selected', key: 'id', label: '', width: 30, lockWidth: true, selector: true }, 50 | { id: 'ID', key: 'id', label: 'ID', sort: 'number', filter: 'number' }, 51 | { id: 'first_name', key: 'first_name', label: 'First Name', sort: 'string', filter: 'like' }, 52 | { id: 'last_name', key: 'last_name', label: 'Last Name', sort: 'string', filter: 'like' }, 53 | { id: 'age', key: 'age', label: 'Age', sort: 'number', filter: 'number' }, 54 | { id: 'height', key: 'height', label: 'Height', format: inches2feet, filter: feet_filter, sort: 'number' }, 55 | { id: 'weight', key: 'weight', label: 'Weight', filter: 'number', sort: 'number' } 56 | ]; 57 | 58 | // Table data 59 | $scope.my_table_data = genRows(30); 60 | $scope.my_table_data2 = genRows(40); 61 | $scope.my_table_data3 = genRows(50); 62 | $scope.my_table_data4 = genRows(60); 63 | $scope.my_table_data5 = genRows(70); 64 | $scope.my_table_data6 = genRows(80); 65 | $scope.my_table_data7 = genRows(90); 66 | 67 | 68 | // Selected rows 69 | $scope.my_selected_rows = []; 70 | $scope.my_selected_rows2 = []; 71 | $scope.my_selected_rows3 = []; 72 | $scope.my_selected_rows4 = []; 73 | $scope.my_selected_rows5 = []; 74 | $scope.my_selected_rows6 = []; 75 | $scope.my_selected_rows7 = []; 76 | 77 | // table options 78 | $scope.my_table_options = { 79 | rowLimit: 10, 80 | storage: localStorage, 81 | storageKey: 'gh-page-table' 82 | }; 83 | 84 | $scope.my_table_options2 = { 85 | rowLimit: 10, 86 | storage: localStorage, 87 | storageKey: 'gh-page-table2' 88 | }; 89 | 90 | $scope.my_table_options3 = { 91 | rowLimit: 10, 92 | storage: localStorage, 93 | storageKey: 'gh-page-table3' 94 | }; 95 | 96 | $scope.my_table_options4 = { 97 | rowLimit: 10, 98 | storage: localStorage, 99 | storageKey: 'gh-page-table4' 100 | }; 101 | 102 | $scope.my_table_options5 = { 103 | rowLimit: 10, 104 | storage: localStorage, 105 | storageKey: 'gh-page-table5' 106 | }; 107 | 108 | $scope.my_table_options6 = { 109 | rowLimit: 10, 110 | storage: localStorage, 111 | storageKey: 'gh-page-table6' 112 | }; 113 | 114 | $scope.my_table_options7 = { 115 | rowLimit: 10, 116 | storage: localStorage, 117 | storageKey: 'gh-page-table7' 118 | }; 119 | 120 | setInterval(function() { 121 | $scope.my_table_data = genRows(30); 122 | $scope.my_table_data2 = genRows(40); 123 | $scope.my_table_data3 = genRows(50); 124 | $scope.my_table_data4 = genRows(60); 125 | $scope.my_table_data5 = genRows(70); 126 | $scope.my_table_data6 = genRows(80); 127 | $scope.my_table_data7 = genRows(90); 128 | // $scope.my_table_data = genRows(30); 129 | $scope.$apply(); 130 | }, 1000); 131 | 132 | }); 133 | -------------------------------------------------------------------------------- /app/scripts/controllers/server-side.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('apMesa.ghPage') 4 | 5 | .controller('ServerSideCtrl', function ($scope, $templateCache, $q, phoneData, $timeout) { 6 | 7 | // Mock serverside call 8 | function getData(offset, limit, activeFilters, activeSorts) { 9 | 10 | var dfd = $q.defer(); 11 | 12 | 13 | // Simulate back-end errors: 14 | 15 | // if (Math.random() < 0.2) { 16 | // $timeout(function() { 17 | // dfd.reject({ message: 'an error occurred' }); 18 | // }, 200); 19 | // return dfd.promise; 20 | // } 21 | 22 | $timeout(function() { 23 | 24 | var results = phoneData; 25 | 26 | // Perform filters first: 27 | if (activeFilters.length) { 28 | results = results.filter(function(row) { 29 | for (var i = 0; i < activeFilters.length; i++) { 30 | var filterMeta = activeFilters[i]; 31 | var columnDef = filterMeta.column; 32 | var filterString = filterMeta.value.toLowerCase(); 33 | 34 | if (columnDef.filter === 'string') { 35 | var searchableValue = row[columnDef.key].toLowerCase(); 36 | // console.log(searchableValue, filterString); 37 | if (searchableValue.indexOf(filterString) === -1) { 38 | return false; 39 | } else { 40 | return true; 41 | } 42 | } 43 | } 44 | }); 45 | } 46 | 47 | // Perform sorts 48 | if (activeSorts.length) { 49 | results = results.sort(function(rowA, rowB) { 50 | for (var i = 0; i < activeSorts.length; i++) { 51 | var sortMeta = activeSorts[i]; 52 | var columnDef = sortMeta.column; 53 | var sortDirection = sortMeta.direction; 54 | 55 | if (columnDef.sort === 'string' && angular.isString(rowA[columnDef.key]) && angular.isString(rowB[columnDef.key])) { 56 | var comparisonResult = sortDirection === 'ASC' ? rowA[columnDef.key].localeCompare(rowB[columnDef.key]) : rowB[columnDef.key].localeCompare(rowA[columnDef.key]); 57 | if (comparisonResult !== 0) { 58 | return comparisonResult; 59 | } 60 | } 61 | } 62 | return 0; 63 | }); 64 | } 65 | 66 | dfd.resolve({ 67 | total: results.length, 68 | rows: results.slice(offset, offset + limit) 69 | }); 70 | 71 | }, Math.random() * 1000); 72 | 73 | return dfd.promise; 74 | } 75 | 76 | $scope.my_table_options = { 77 | getData: getData 78 | }; 79 | 80 | $scope.my_table_options_paginate = angular.extend({}, $scope.my_table_options, { 81 | pagingStrategy: 'PAGINATE' 82 | }); 83 | 84 | $scope.my_table_columns = [ 85 | { 86 | id: 'DeviceName', 87 | label: 'Device Name', 88 | key: 'DeviceName', 89 | // The filter and sort values are only checked for truthiness by angular-mesa when getData is used 90 | filter: 'string', 91 | filterPlaceholder: 'filter name', 92 | sort: 'string' 93 | }, 94 | { 95 | id: 'Brand', 96 | label: 'Brand', 97 | key: 'Brand', 98 | filter: 'string', 99 | filterPlaceholder: 'filter brand', 100 | sort: 'string' 101 | }, 102 | { 103 | id: 'weight', 104 | label: 'weight', 105 | key: 'weight', 106 | sort: 'string' 107 | }, 108 | { 109 | id: 'size', 110 | label: 'size', 111 | key: 'size' 112 | } 113 | ]; 114 | 115 | }); 116 | -------------------------------------------------------------------------------- /app/views/clickable-rows.html: -------------------------------------------------------------------------------- 1 |
Clickable Rows
2 | 3 |

This example shows the usage of the clickable rows feature, where you can define a custom click handler for each row.

4 |
5 | 6 | 16 | 17 | 24 | 25 | -------------------------------------------------------------------------------- /app/views/disabled-columns.html: -------------------------------------------------------------------------------- 1 |
Disabled Columns
2 | 3 |

This example shows how you can enable/disable columns dynamically.

4 |
5 | 6 |
7 |
8 |
9 | 10 |
11 | 17 | 23 |
24 | 25 |
26 | 33 | 34 |
35 | 36 |
37 | 44 | 45 |
46 | 47 |
48 |
49 | 50 |

Enabled Columns

51 | 52 |
53 | 54 | {{ column.label }} 55 |
56 | 57 |
{{ vm.my_enabled_columns | json }}
58 | 59 |
60 |
61 |
62 | -------------------------------------------------------------------------------- /app/views/expandable-panel.html: -------------------------------------------------------------------------------- 1 |

Specs for {{ row.DeviceName }}:

2 |
3 |
4 | gprs: {{ row.gprs }}
5 | edge: {{ row.edge }}
6 | announced: {{ row.announced }}
7 | status: {{ row.status }}
8 | dimensions: {{ row.dimensions }}
9 | weight: {{ row.weight }}
10 | sim: {{ row.sim }}
11 | type: {{ row.type }}
12 | size: {{ row.size }}
13 | resolution: {{ row.resolution }}
14 | card_slot: {{ row.card_slot }}
15 | alert_types: {{ row.alert_types }}
16 | loudspeaker_: {{ row.loudspeaker_ }} 17 |
18 |
19 | wlan: {{ row.wlan }}
20 | bluetooth: {{ row.bluetooth }}
21 | gps: {{ row.gps }}
22 | radio: {{ row.radio }}
23 | usb: {{ row.usb }}
24 | messaging: {{ row.messaging }}
25 | browser: {{ row.browser }}
26 | java: {{ row.java }}
27 | features_c: {{ row.features_c }}/video editor',
28 | battery_c: {{ row.battery_c }}
29 | stand_by: {{ row.stand_by }}
30 | talk_time: {{ row.talk_time }}
31 | colors: {{ row.colors }} 32 |
33 |
34 | sensors: {{ row.sensors }}
35 | cpu: {{ row.cpu }}
36 | internal: {{ row.internal }}
37 | os: {{ row.os }}
38 | primary_: {{ row.primary_ }}
39 | video: {{ row.video }}
40 | secondary: {{ row.secondary }}
41 | speed: {{ row.speed }}
42 | network_c: {{ row.network_c }}
43 | chipset: {{ row.chipset }}
44 | features: {{ row.features }}
45 | gpu: {{ row.gpu }}
46 | multitouch: {{ row.multitouch }}
47 | _2g_bands: {{ row._2g_bands }}
48 | _3_5mm_jack_: {{ row._3_5mm_jack_ }}
49 | _3g_bands: {{ row._3g_bands }} 50 |
51 |
-------------------------------------------------------------------------------- /app/views/expandable.html: -------------------------------------------------------------------------------- 1 |
Expandable Rows
2 | 3 |

This example shows the usage of the expandable rows feature, where rows can be expanded with arbitrary content.

4 |
5 | 6 |
7 |
8 | 14 | 20 |
21 |
22 | 28 | 29 |
30 | 31 |
32 | 38 | 39 |
40 |
41 | -------------------------------------------------------------------------------- /app/views/main.html: -------------------------------------------------------------------------------- 1 |
2 |
Description
3 |

An angular table directive catered to displaying real-time data. For many situations, this can be a suitable replacement or alternative for ng-grid/ui-grid.

4 |
Features
5 |
6 |
7 |
    8 |
  • complex column-specific filters
  • 9 |
  • column sorting
  • 10 |
  • column resizing
  • 11 |
  • row selection
  • 12 |
13 |
14 |
15 |
    16 |
  • stacked ordering
  • 17 |
  • localStorage/custom state persistance
  • 18 |
  • rows are virtualized (can handle a lot of rows)
  • 19 |
  • pagination
  • 20 |
21 |
22 |
23 |
24 | 25 |
26 | 27 |
Kitchen Sink Example:
28 | 29 |

This example showcases most of the features available for this module. Specifically, it shows row selection, column sorting, built-in column filters, custom column filters, column resizing, locked column width, cell formatting, template string for cell markup, templateUrl for cell markup, and use of localStorage for persistence between page loads.

30 |
31 |
32 | 33 |
34 |
35 | 41 | 47 |
48 |
49 | 56 | 57 |
58 |

Selected rows:

59 |
{{my_selected_rows | json}}
60 |
61 |
62 | 63 |
64 | 71 | 72 |
73 |

Selected rows:

74 |
{{my_selected_rows | json}}
75 |
76 |
77 |
78 | -------------------------------------------------------------------------------- /app/views/max-height.html: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | This content should be pushed down as more rows come into the table. 10 | -------------------------------------------------------------------------------- /app/views/perf.html: -------------------------------------------------------------------------------- 1 |
Performance Test:
2 | 3 |

This page tests the performance of 7 independent instances of tables on the page, each with a significant number of rows.

4 |
5 |
6 | 7 | 14 | 15 | 16 | 23 | 24 | 25 | 32 | 33 | 34 | 41 | 42 | 43 | 50 | 51 | 52 | 59 | 60 | 61 | 68 | 69 | -------------------------------------------------------------------------------- /app/views/server-side-interaction.html: -------------------------------------------------------------------------------- 1 |
Server-side Interaction
2 | 3 |

This example shows the usage of the server-side interaction feature, where rows can be fetched from a back-end.

4 |
5 | 6 |
7 | 13 | 19 |
20 | 21 | 26 | 27 | 28 | 33 | 34 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angularjs-table", 3 | "version": "2.21.5", 4 | "main": [ 5 | "./dist/ap-mesa.js", 6 | "./dist/ap-mesa.css" 7 | ], 8 | "dependencies": { 9 | "angular": "^1.6", 10 | "jquery": "~2.0.3", 11 | "angular-sanitize": "^1.6", 12 | "angular-ui-sortable": "~0.13", 13 | "jquery-ui": ">=1.11.0" 14 | }, 15 | "devDependencies": { 16 | "angular-route": "^1.6", 17 | "bootstrap": "~3.0.3", 18 | "angular-mocks": "^1.6", 19 | "angular-animate": "^1.6", 20 | "mocha": "~1.18.2", 21 | "chai": "~1.9.1", 22 | "sinon": "~1.9.0", 23 | "sinon-chai": "~2.5.0" 24 | }, 25 | "ignore": [ 26 | "src", 27 | "vendor", 28 | "app", 29 | "node_modules", 30 | "test", 31 | "**/.*" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /dist/ap-mesa.css: -------------------------------------------------------------------------------- 1 | /* main root element */ 2 | 3 | .ap-mesa-wrapper { 4 | position: relative; 5 | } 6 | 7 | /* styles for both header- and rows- tables */ 8 | 9 | .ap-mesa { 10 | table-layout: fixed; 11 | width: 100%; 12 | margin-bottom: 0; 13 | } 14 | 15 | /* the visible table header */ 16 | 17 | .mesa-header-table { 18 | border-bottom: none; 19 | } 20 | 21 | .mesa-header-table thead > tr > th { 22 | border-width: 1px; 23 | } 24 | 25 | /* the invisible table header; used for correct column widths */ 26 | 27 | .mesa-rows-table thead { 28 | height: 0; 29 | visibility: hidden; 30 | } 31 | 32 | .mesa-rows-table > thead > tr > th { 33 | border-width: 0; 34 | padding: 0 !important; 35 | } 36 | 37 | .mesa-rows-table-wrapper { 38 | overflow: auto; 39 | } 40 | 41 | .mesa-rows-table > tbody + tbody { 42 | border-top: none; 43 | } 44 | 45 | /* general styles for all cells in both tables */ 46 | 47 | .ap-mesa th { 48 | white-space: nowrap; 49 | position: relative; 50 | } 51 | 52 | .ap-mesa td { 53 | word-wrap: break-word; 54 | overflow: hidden; 55 | } 56 | 57 | /* this type of cell is used to show messages like "loading" */ 58 | 59 | .ap-mesa td.space-holder-row-cell { 60 | text-align: center; 61 | } 62 | 63 | /* search input */ 64 | 65 | .ap-mesa tr.ap-mesa-filter-row td input { 66 | width: 100%; 67 | border-radius: 2em; 68 | border: 1px solid #CCC; 69 | outline: none; 70 | text-indent: 0.3em; 71 | font-size: 90%; 72 | } 73 | 74 | .ap-mesa tr.ap-mesa-filter-row td { 75 | position: relative; 76 | } 77 | 78 | /* button to clear search */ 79 | 80 | .ap-mesa tr.ap-mesa-filter-row td .clear-search-btn { 81 | position: absolute; 82 | border-radius: 50%; 83 | border: none; 84 | right: 1em; 85 | top: 50%; 86 | -webkit-transform: translateY(-50%); 87 | -ms-transform: translateY(-50%); 88 | transform: translateY(-50%); 89 | font-size: 12px; 90 | opacity: 0.2; 91 | color: white; 92 | background-color: black; 93 | padding: 0; 94 | width: 15px; 95 | line-height: 15px; 96 | } 97 | 98 | /* activated search input, as in when there is text in it */ 99 | 100 | .ap-mesa tr.ap-mesa-filter-row td input.active { 101 | background-color: #3D82C2; 102 | color: #FFF; 103 | border-color: #747474; 104 | } 105 | 106 | /* placeholder object for when columns are being sorted */ 107 | 108 | .ap-mesa .ap-mesa-column-placeholder { 109 | background-color: #DDD; 110 | } 111 | 112 | /* handle to grab in order to resize a column */ 113 | 114 | .ap-mesa th .column-resizer { 115 | position: absolute; 116 | top: 0; 117 | right: 0; 118 | width: 5px; 119 | height: 100%; 120 | border-width: 0 1px; 121 | cursor: col-resize; 122 | border-color: #DDD; 123 | border-style: solid; 124 | } 125 | 126 | /* this class is applied to a .column-resizer 127 | when a discreet width has been set on it */ 128 | 129 | .ap-mesa th .column-resizer.discreet-width { 130 | background-color: #DDD; 131 | } 132 | 133 | /* wrapper for text in a th */ 134 | 135 | .ap-mesa th .column-text { 136 | max-width: 100%; 137 | overflow: hidden; 138 | display: block; 139 | } 140 | 141 | /* the element showing what the new width will be */ 142 | 143 | .ap-mesa th .column-resizer-marquee { 144 | left: 0; 145 | top: 0; 146 | height: 100%; 147 | border: 1px dotted #DEDEDE; 148 | position: absolute; 149 | } 150 | 151 | /* sorting icons */ 152 | 153 | .ap-mesa th span.sorting-icon { 154 | font-size: 10px; 155 | } 156 | 157 | .ap-mesa th span.sort-priority { 158 | background-color: black; 159 | color: white; 160 | font-size: 9px; 161 | font-weight: bold; 162 | vertical-align: top; 163 | display: inline-block; 164 | height: 12px; 165 | width: 12px; 166 | text-align: center; 167 | border-radius: 50%; 168 | } 169 | 170 | .ap-mesa th span.glyphicon-sort { 171 | opacity: 0.2; 172 | } 173 | 174 | .ap-mesa th.sortable-column { 175 | cursor: pointer; 176 | -webkit-user-select: none; 177 | -moz-user-select: none; 178 | -ms-user-select: none; 179 | user-select: none; 180 | } 181 | 182 | /* dummy row */ 183 | 184 | .ap-mesa-dummy-row { 185 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAlCAYAAACDKIOpAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAABtJREFUeNpiuHv37n+G////MzAxAMHQIQACDAC7twbaN2nkgwAAAABJRU5ErkJggg=='); 186 | background-repeat: repeat; 187 | } 188 | 189 | table tbody .ap-mesa-dummy-row td { 190 | border-top: none; 191 | padding: 0; 192 | } 193 | 194 | .ap-mesa-pagination { 195 | -webkit-user-select: none; 196 | -moz-user-select: none; 197 | -ms-user-select: none; 198 | user-select: none; 199 | overflow: hidden; 200 | } 201 | 202 | .ap-mesa-pagination .rows-per-page-ctrl { 203 | float: right; 204 | } 205 | 206 | .ap-mesa-pagination .rows-per-page-ctrl .pagination { 207 | vertical-align: middle; 208 | } 209 | 210 | .ap-mesa-pagination .rows-per-page-msg { 211 | vertical-align: middle; 212 | } 213 | 214 | .ap-mesa-pagination ul.pagination li a { 215 | cursor: pointer; 216 | } 217 | 218 | /* async loader styles */ 219 | 220 | .ap-mesa-status-display-wrapper { 221 | position: relative; 222 | text-align: center; 223 | } 224 | 225 | .ap-mesa-status-display { 226 | background: rgba(0,0,0,0.3); 227 | border-radius: 1rem; 228 | width: 20%; 229 | max-width: 130px; 230 | min-width: 100px; 231 | height: auto; 232 | padding: 0.5rem 1rem; 233 | position: relative; 234 | display: inline-block; 235 | margin-top: 0.7rem; 236 | } 237 | 238 | .ap-mesa-loading-display.ng-enter { 239 | -webkit-transition: opacity cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; 240 | transition: opacity cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; 241 | } 242 | 243 | .paging-strategy-scroll .ap-mesa-loading-display.ng-enter { 244 | -webkit-transition-delay: 0.4s; 245 | transition-delay: 0.4s; 246 | } 247 | 248 | .paging-strategy-paginate .ap-mesa-loading-display.ng-enter { 249 | -webkit-transition-delay: 0s; 250 | transition-delay: 0s; 251 | } 252 | 253 | .ap-mesa-loading-display.ng-enter, 254 | .ap-mesa-loading-display.ng-enter.ng-leave.ng-leave-active { 255 | opacity: 0; 256 | } 257 | 258 | .ap-mesa-loading-display.ng-enter.ng-leave, 259 | .ap-mesa-loading-display.ng-enter.ng-enter-active { 260 | opacity: 1; 261 | } 262 | 263 | .ap-mesa-status-display-wrapper.has-rows .ap-mesa-status-display { 264 | position: absolute; 265 | -webkit-transform: translateX(-50%); 266 | -ms-transform: translateX(-50%); 267 | transform: translateX(-50%); 268 | } 269 | 270 | .ap-mesa-error-display-inner { 271 | color: red; 272 | display: inline-block; 273 | } 274 | 275 | /* credit to: https://projects.lukehaas.me/css-loaders/ */ 276 | 277 | .ap-mesa-status-display-inner, 278 | .ap-mesa-status-display-inner:before, 279 | .ap-mesa-status-display-inner:after { 280 | border-radius: 50%; 281 | width: 1.5em; 282 | height: 1.5em; 283 | -webkit-animation-fill-mode: both; 284 | animation-fill-mode: both; 285 | -webkit-animation: load7 1.8s infinite ease-in-out; 286 | animation: load7 1.8s infinite ease-in-out; 287 | } 288 | 289 | .ap-mesa-status-display-inner { 290 | color: #ffffff; 291 | font-size: 10px; 292 | margin: 0 auto; 293 | position: relative; 294 | text-indent: -9999em; 295 | -webkit-transform: translateZ(0) translateY(-100%); 296 | -ms-transform: translateZ(0) translateY(-100%); 297 | transform: translateZ(0) translateY(-100%); 298 | -webkit-animation-delay: -0.16s; 299 | animation-delay: -0.16s; 300 | } 301 | 302 | .ap-mesa-status-display-inner:before, 303 | .ap-mesa-status-display-inner:after { 304 | content: ''; 305 | position: absolute; 306 | top: 0; 307 | } 308 | 309 | .ap-mesa-status-display-inner:before { 310 | left: -3.5em; 311 | -webkit-animation-delay: -0.32s; 312 | animation-delay: -0.32s; 313 | } 314 | 315 | .ap-mesa-status-display-inner:after { 316 | left: 3.5em; 317 | } 318 | 319 | @-webkit-keyframes load7 { 320 | 0%, 80%, 100% { 321 | -webkit-box-shadow: 0 1.5em 0 -1.3em; 322 | box-shadow: 0 1.5em 0 -1.3em; 323 | } 324 | 325 | 40% { 326 | -webkit-box-shadow: 0 1.5em 0 0; 327 | box-shadow: 0 1.5em 0 0; 328 | } 329 | } 330 | 331 | @keyframes load7 { 332 | 0%, 80%, 100% { 333 | -webkit-box-shadow: 0 1.5em 0 -1.3em; 334 | box-shadow: 0 1.5em 0 -1.3em; 335 | } 336 | 337 | 40% { 338 | -webkit-box-shadow: 0 1.5em 0 0; 339 | box-shadow: 0 1.5em 0 0; 340 | } 341 | } -------------------------------------------------------------------------------- /dist/ap-mesa.min.css: -------------------------------------------------------------------------------- 1 | .ap-mesa-wrapper{position:relative}.ap-mesa{table-layout:fixed;width:100%;margin-bottom:0}.mesa-header-table{border-bottom:0}.mesa-header-table thead>tr>th{border-width:1px}.mesa-rows-table thead{height:0;visibility:hidden}.mesa-rows-table>thead>tr>th{border-width:0;padding:0!important}.mesa-rows-table-wrapper{overflow:auto}.mesa-rows-table>tbody+tbody{border-top:0}.ap-mesa th{white-space:nowrap;position:relative}.ap-mesa td{word-wrap:break-word;overflow:hidden}.ap-mesa td.space-holder-row-cell{text-align:center}.ap-mesa tr.ap-mesa-filter-row td input{width:100%;border-radius:2em;border:1px solid #CCC;outline:0;text-indent:.3em;font-size:90%}.ap-mesa tr.ap-mesa-filter-row td{position:relative}.ap-mesa tr.ap-mesa-filter-row td .clear-search-btn{position:absolute;border-radius:50%;border:0;right:1em;top:50%;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%);font-size:12px;opacity:.2;color:#fff;background-color:#000;padding:0;width:15px;line-height:15px}.ap-mesa tr.ap-mesa-filter-row td input.active{background-color:#3D82C2;color:#FFF;border-color:#747474}.ap-mesa .ap-mesa-column-placeholder{background-color:#DDD}.ap-mesa th .column-resizer{position:absolute;top:0;right:0;width:5px;height:100%;border-width:0 1px;cursor:col-resize;border-color:#DDD;border-style:solid}.ap-mesa th .column-resizer.discreet-width{background-color:#DDD}.ap-mesa th .column-text{max-width:100%;overflow:hidden;display:block}.ap-mesa th .column-resizer-marquee{left:0;top:0;height:100%;border:1px dotted #DEDEDE;position:absolute}.ap-mesa th span.sorting-icon{font-size:10px}.ap-mesa th span.sort-priority{background-color:#000;color:#fff;font-size:9px;font-weight:700;vertical-align:top;display:inline-block;height:12px;width:12px;text-align:center;border-radius:50%}.ap-mesa th span.glyphicon-sort{opacity:.2}.ap-mesa th.sortable-column{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ap-mesa-dummy-row{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAlCAYAAACDKIOpAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAABtJREFUeNpiuHv37n+G////MzAxAMHQIQACDAC7twbaN2nkgwAAAABJRU5ErkJggg==);background-repeat:repeat}table tbody .ap-mesa-dummy-row td{border-top:0;padding:0}.ap-mesa-pagination{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:hidden}.ap-mesa-pagination .rows-per-page-ctrl{float:right}.ap-mesa-pagination .rows-per-page-ctrl .pagination,.ap-mesa-pagination .rows-per-page-msg{vertical-align:middle}.ap-mesa-pagination ul.pagination li a{cursor:pointer}.ap-mesa-status-display-wrapper{position:relative;text-align:center}.ap-mesa-status-display{background:rgba(0,0,0,.3);border-radius:1rem;width:20%;max-width:130px;min-width:100px;height:auto;padding:.5rem 1rem;position:relative;display:inline-block;margin-top:.7rem}.ap-mesa-loading-display.ng-enter{-webkit-transition:opacity cubic-bezier(0.25,.46,.45,.94) .5s;transition:opacity cubic-bezier(0.25,.46,.45,.94) .5s}.paging-strategy-scroll .ap-mesa-loading-display.ng-enter{-webkit-transition-delay:.4s;transition-delay:.4s}.paging-strategy-paginate .ap-mesa-loading-display.ng-enter{-webkit-transition-delay:0s;transition-delay:0s}.ap-mesa-loading-display.ng-enter,.ap-mesa-loading-display.ng-enter.ng-leave.ng-leave-active{opacity:0}.ap-mesa-loading-display.ng-enter.ng-enter-active,.ap-mesa-loading-display.ng-enter.ng-leave{opacity:1}.ap-mesa-status-display-wrapper.has-rows .ap-mesa-status-display{position:absolute;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%)}.ap-mesa-error-display-inner{color:red;display:inline-block}.ap-mesa-status-display-inner,.ap-mesa-status-display-inner:after,.ap-mesa-status-display-inner:before{border-radius:50%;width:1.5em;height:1.5em;-webkit-animation:load7 1.8s infinite ease-in-out;animation:load7 1.8s infinite ease-in-out}.ap-mesa-status-display-inner{color:#fff;font-size:10px;margin:0 auto;position:relative;text-indent:-9999em;-webkit-transform:translateZ(0) translateY(-100%);-ms-transform:translateZ(0) translateY(-100%);transform:translateZ(0) translateY(-100%);-webkit-animation-delay:-.16s;animation-delay:-.16s}.ap-mesa-status-display-inner:after,.ap-mesa-status-display-inner:before{content:'';position:absolute;top:0}.ap-mesa-status-display-inner:before{left:-3.5em;-webkit-animation-delay:-.32s;animation-delay:-.32s}.ap-mesa-status-display-inner:after{left:3.5em}@-webkit-keyframes load7{0%,100%,80%{-webkit-box-shadow:0 1.5em 0 -1.3em;box-shadow:0 1.5em 0 -1.3em}40%{-webkit-box-shadow:0 1.5em 0 0;box-shadow:0 1.5em 0 0}}@keyframes load7{0%,100%,80%{-webkit-box-shadow:0 1.5em 0 -1.3em;box-shadow:0 1.5em 0 -1.3em}40%{-webkit-box-shadow:0 1.5em 0 0;box-shadow:0 1.5em 0 0}} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angularjs-table", 3 | "version": "2.21.5", 4 | "license": "Apache License, v2.0", 5 | "dependencies": { 6 | "angular": "^1.6", 7 | "jquery": "~2", 8 | "angular-sanitize": "~1.3" 9 | }, 10 | "repository": "https://github.com/andyperlitch/angularjs-table", 11 | "types": "./angular-mesa.d.ts", 12 | "main": "dist/ap-mesa.js", 13 | "style": "dist/ap-mesa.css", 14 | "devDependencies": { 15 | "chai": "^1.9.1", 16 | "grunt": "^1.0.1", 17 | "grunt-autoprefixer": "~0.4.0", 18 | "grunt-bower-install": "~0.7.0", 19 | "grunt-concurrent": "~0.4.1", 20 | "grunt-contrib-clean": "~0.5.0", 21 | "grunt-contrib-coffee": "~0.7.0", 22 | "grunt-contrib-compass": "~0.6.0", 23 | "grunt-contrib-concat": "~0.3.0", 24 | "grunt-contrib-connect": "~0.5.0", 25 | "grunt-contrib-copy": "~0.4.1", 26 | "grunt-contrib-cssmin": "~0.7.0", 27 | "grunt-contrib-htmlmin": "~0.1.3", 28 | "grunt-contrib-jshint": "~0.7.1", 29 | "grunt-contrib-uglify": "~0.2.0", 30 | "grunt-contrib-watch": "~0.5.2", 31 | "grunt-google-cdn": "~0.2.0", 32 | "grunt-html2js": "^0.2.4", 33 | "grunt-injector": "^0.5.4", 34 | "grunt-karma": "^0.12.2", 35 | "grunt-newer": "~0.5.4", 36 | "grunt-ngmin": "~0.0.2", 37 | "grunt-rev": "~0.1.0", 38 | "grunt-svgmin": "~0.2.0", 39 | "grunt-usemin": "~2.0.0", 40 | "jshint-stylish": "~0.1.3", 41 | "karma": "^1.6.0", 42 | "karma-chai": "^0.1.0", 43 | "karma-chrome-launcher": "^0.1.2", 44 | "karma-coverage": "^0.2.7", 45 | "karma-firefox-launcher": "^0.1.3", 46 | "karma-mocha": "^0.1.3", 47 | "karma-ng-html2js-preprocessor": "^0.1.0", 48 | "karma-phantomjs-launcher": "^1.0.4", 49 | "karma-sinon": "^1.0.4", 50 | "karma-sinon-chai": "^0.1.5", 51 | "load-grunt-tasks": "~0.2.0", 52 | "mocha": "^1.18.2", 53 | "sinon": "^1.12.2", 54 | "time-grunt": "~0.2.1" 55 | }, 56 | "engines": { 57 | "node": ">=0.8.0" 58 | }, 59 | "scripts": { 60 | "test": "grunt", 61 | "gh-pages": "bash update-gh-pages.sh" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/ap-mesa.css: -------------------------------------------------------------------------------- 1 | /* main root element */ 2 | .ap-mesa-wrapper { 3 | position: relative; 4 | } 5 | 6 | /* styles for both header- and rows- tables */ 7 | .ap-mesa { 8 | table-layout: fixed; 9 | width: 100%; 10 | margin-bottom: 0; 11 | } 12 | 13 | /* the visible table header */ 14 | .mesa-header-table { 15 | border-bottom: none; 16 | } 17 | .mesa-header-table thead > tr > th { 18 | border-width: 1px; 19 | } 20 | 21 | /* the invisible table header; used for correct column widths */ 22 | .mesa-rows-table thead { 23 | height: 0; 24 | visibility: hidden; 25 | } 26 | .mesa-rows-table > thead > tr > th{ 27 | border-width: 0; 28 | padding: 0 !important; 29 | } 30 | .mesa-rows-table-wrapper { 31 | overflow: auto; 32 | } 33 | .mesa-rows-table > tbody + tbody { 34 | border-top: none; 35 | } 36 | 37 | /* general styles for all cells in both tables */ 38 | .ap-mesa th { 39 | white-space: nowrap; 40 | position: relative; 41 | } 42 | .ap-mesa td { 43 | word-wrap: break-word; 44 | overflow: hidden; 45 | } 46 | 47 | /* this type of cell is used to show messages like "loading" */ 48 | .ap-mesa td.space-holder-row-cell { 49 | text-align: center; 50 | } 51 | 52 | /* search input */ 53 | .ap-mesa tr.ap-mesa-filter-row td input { 54 | width: 100%; 55 | border-radius: 2em; 56 | border: 1px solid #CCC; 57 | outline: none; 58 | text-indent: 0.3em; 59 | font-size: 90%; 60 | } 61 | .ap-mesa tr.ap-mesa-filter-row td { 62 | position: relative; 63 | } 64 | /* button to clear search */ 65 | .ap-mesa tr.ap-mesa-filter-row td .clear-search-btn { 66 | position: absolute; 67 | border-radius: 50%; 68 | border: none; 69 | right: 1em; 70 | top: 50%; 71 | transform: translateY(-50%); 72 | font-size: 12px; 73 | opacity: 0.2; 74 | color: white; 75 | background-color: black; 76 | padding: 0; 77 | width: 15px; 78 | line-height: 15px; 79 | } 80 | 81 | /* activated search input, as in when there is text in it */ 82 | .ap-mesa tr.ap-mesa-filter-row td input.active { 83 | background-color: #3D82C2; 84 | color: #FFF; 85 | border-color: #747474; 86 | } 87 | 88 | /* placeholder object for when columns are being sorted */ 89 | .ap-mesa .ap-mesa-column-placeholder { 90 | background-color: #DDD; 91 | } 92 | 93 | /* handle to grab in order to resize a column */ 94 | .ap-mesa th .column-resizer { 95 | position: absolute; 96 | top:0; 97 | right: 0; 98 | width: 5px; 99 | height: 100%; 100 | border-width: 0 1px; 101 | cursor: col-resize; 102 | border-color: #DDD; 103 | border-style: solid; 104 | } 105 | 106 | /* this class is applied to a .column-resizer 107 | when a discreet width has been set on it */ 108 | .ap-mesa th .column-resizer.discreet-width { 109 | background-color: #DDD; 110 | } 111 | 112 | /* wrapper for text in a th */ 113 | .ap-mesa th .column-text { 114 | max-width: 100%; 115 | overflow: hidden; 116 | display: block; 117 | } 118 | 119 | /* the element showing what the new width will be */ 120 | .ap-mesa th .column-resizer-marquee { 121 | left: 0; 122 | top: 0; 123 | height: 100%; 124 | border: 1px dotted #DEDEDE; 125 | position: absolute; 126 | } 127 | 128 | /* sorting icons */ 129 | .ap-mesa th span.sorting-icon { 130 | font-size: 10px; 131 | } 132 | .ap-mesa th span.sort-priority { 133 | background-color: black; 134 | color: white; 135 | font-size: 9px; 136 | font-weight: bold; 137 | vertical-align: top; 138 | display: inline-block; 139 | height: 12px; 140 | width: 12px; 141 | text-align: center; 142 | border-radius: 50%; 143 | } 144 | 145 | .ap-mesa th span.glyphicon-sort { 146 | opacity: 0.2; 147 | } 148 | .ap-mesa th.sortable-column { 149 | cursor: pointer; 150 | user-select: none 151 | } 152 | 153 | /* dummy row */ 154 | .ap-mesa-dummy-row { 155 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAlCAYAAACDKIOpAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAABtJREFUeNpiuHv37n+G////MzAxAMHQIQACDAC7twbaN2nkgwAAAABJRU5ErkJggg=='); 156 | background-repeat: repeat; 157 | } 158 | table tbody .ap-mesa-dummy-row td { 159 | border-top: none; 160 | padding: 0; 161 | } 162 | .ap-mesa-pagination { 163 | user-select: none; 164 | overflow: hidden; 165 | } 166 | .ap-mesa-pagination .rows-per-page-ctrl { 167 | float: right; 168 | } 169 | .ap-mesa-pagination .rows-per-page-ctrl .pagination { 170 | vertical-align: middle; 171 | } 172 | .ap-mesa-pagination .rows-per-page-msg { 173 | vertical-align: middle; 174 | } 175 | .ap-mesa-pagination ul.pagination li a { 176 | cursor: pointer; 177 | } 178 | 179 | /* async loader styles */ 180 | .ap-mesa-status-display-wrapper { 181 | position: relative; 182 | text-align: center; 183 | } 184 | .ap-mesa-status-display { 185 | background: rgba(0,0,0,0.3); 186 | border-radius: 1rem; 187 | width: 20%; 188 | max-width: 130px; 189 | min-width: 100px; 190 | height: auto; 191 | padding: 0.5rem 1rem; 192 | position: relative; 193 | display: inline-block; 194 | margin-top: 0.7rem; 195 | } 196 | .ap-mesa-loading-display.ng-enter { 197 | transition: opacity cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; 198 | } 199 | .paging-strategy-scroll .ap-mesa-loading-display.ng-enter { 200 | transition-delay: 0.4s; 201 | } 202 | .paging-strategy-paginate .ap-mesa-loading-display.ng-enter { 203 | transition-delay: 0s; 204 | } 205 | .ap-mesa-loading-display.ng-enter, 206 | .ap-mesa-loading-display.ng-enter.ng-leave.ng-leave-active { 207 | opacity:0; 208 | } 209 | 210 | .ap-mesa-loading-display.ng-enter.ng-leave, 211 | .ap-mesa-loading-display.ng-enter.ng-enter-active { 212 | opacity:1; 213 | } 214 | 215 | .ap-mesa-status-display-wrapper.has-rows .ap-mesa-status-display { 216 | position: absolute; 217 | transform: translateX(-50%); 218 | } 219 | 220 | .ap-mesa-error-display-inner { 221 | color: red; 222 | display: inline-block; 223 | } 224 | 225 | /* credit to: https://projects.lukehaas.me/css-loaders/ */ 226 | .ap-mesa-status-display-inner, 227 | .ap-mesa-status-display-inner:before, 228 | .ap-mesa-status-display-inner:after { 229 | border-radius: 50%; 230 | width: 1.5em; 231 | height: 1.5em; 232 | -webkit-animation-fill-mode: both; 233 | animation-fill-mode: both; 234 | -webkit-animation: load7 1.8s infinite ease-in-out; 235 | animation: load7 1.8s infinite ease-in-out; 236 | } 237 | .ap-mesa-status-display-inner { 238 | color: #ffffff; 239 | font-size: 10px; 240 | margin: 0 auto; 241 | position: relative; 242 | text-indent: -9999em; 243 | -webkit-transform: translateZ(0) translateY(-100%); 244 | -ms-transform: translateZ(0) translateY(-100%); 245 | transform: translateZ(0) translateY(-100%); 246 | -webkit-animation-delay: -0.16s; 247 | animation-delay: -0.16s; 248 | } 249 | .ap-mesa-status-display-inner:before, 250 | .ap-mesa-status-display-inner:after { 251 | content: ''; 252 | position: absolute; 253 | top: 0; 254 | } 255 | .ap-mesa-status-display-inner:before { 256 | left: -3.5em; 257 | -webkit-animation-delay: -0.32s; 258 | animation-delay: -0.32s; 259 | } 260 | .ap-mesa-status-display-inner:after { 261 | left: 3.5em; 262 | } 263 | @-webkit-keyframes load7 { 264 | 0%, 265 | 80%, 266 | 100% { 267 | box-shadow: 0 1.5em 0 -1.3em; 268 | } 269 | 40% { 270 | box-shadow: 0 1.5em 0 0; 271 | } 272 | } 273 | @keyframes load7 { 274 | 0%, 275 | 80%, 276 | 100% { 277 | box-shadow: 0 1.5em 0 -1.3em; 278 | } 279 | 40% { 280 | box-shadow: 0 1.5em 0 0; 281 | } 282 | } 283 | 284 | -------------------------------------------------------------------------------- /src/ap-mesa.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | angular.module('apMesa', [ 19 | 'apMesa.templates', 20 | 'ui.sortable', 21 | 'ngSanitize', 22 | 'apMesa.directives.apMesa' 23 | ]); 24 | -------------------------------------------------------------------------------- /src/controllers/ApMesaController.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | angular.module('apMesa.controllers.ApMesaController', [ 19 | 'apMesa.services.apMesaSortFunctions', 20 | 'apMesa.services.apMesaFilterFunctions', 21 | 'apMesa.services.apMesaFormatFunctions' 22 | ]) 23 | 24 | .controller('ApMesaController', 25 | ['$scope','$element','apMesaFormatFunctions','apMesaSortFunctions','apMesaFilterFunctions','$log', '$window', '$filter', '$timeout', '$q', function($scope, $element, formats, sorts, filters, $log, $window, $filter, $timeout, $q) { 26 | var CONSTANTS = { 27 | minWidth: 40 28 | }; 29 | // SCOPE FUNCTIONS 30 | $scope.getSelectableRows = function() { 31 | var tableRowFilter = $filter('apMesaRowFilter'); 32 | return angular.isArray($scope.rows) ? tableRowFilter($scope.rows, $scope.columns, $scope.persistentState, $scope.transientState) : []; 33 | }; 34 | 35 | $scope.isSelectedAll = function() { 36 | if (!angular.isArray($scope.rows) || ! angular.isArray($scope.selected)) { 37 | return false; 38 | } 39 | var rows = $scope.getSelectableRows(); 40 | return (rows.length > 0 && rows.length === $scope.selected.length ); 41 | }; 42 | 43 | $scope.selectAll = function() { 44 | $scope.deselectAll(); 45 | // Get a list of filtered rows 46 | var rows = $scope.getSelectableRows(); 47 | if (rows.length <= 0) return; 48 | var columns = $scope.columns; 49 | var selectorKey = null; 50 | var selectObject = null; 51 | // Search for selector key in selector column 52 | for (var i=0; i< columns.length; i++) { 53 | if (columns[i].selector) { 54 | selectorKey = columns[i].key; 55 | selectObject = columns[i].selectObject; 56 | break; 57 | } 58 | } 59 | // Verify that selectorKey was found 60 | if (!selectorKey) { 61 | throw new Error('Unable to find selector column key for selectAll'); 62 | } 63 | //select key or entire object from all rows 64 | for ( var i = 0; i < rows.length; i++) { 65 | $scope.selected.push(selectObject ? rows[i] : rows[i][selectorKey]); 66 | } 67 | }; 68 | 69 | $scope.deselectAll = function() { 70 | while($scope.selected.length > 0) { 71 | $scope.selected.pop(); 72 | } 73 | }; 74 | 75 | $scope.toggleSelectAll = function($event) { 76 | var checkbox = $event.target; 77 | if (checkbox.checked) { 78 | $scope.selectAll(); 79 | } else { 80 | $scope.deselectAll(); 81 | } 82 | }; 83 | 84 | function findSortItemIndex(id) { 85 | var sortLen = $scope.persistentState.sortOrder.length; 86 | for (var i = 0; i < sortLen; i++) { 87 | if ($scope.persistentState.sortOrder[i].id === id) { 88 | return i; 89 | } 90 | } 91 | } 92 | 93 | function findSortItem(id) { 94 | var index = findSortItemIndex(id); 95 | if (index > -1) { 96 | return $scope.persistentState.sortOrder[index]; 97 | } 98 | } 99 | 100 | $scope.addSort = function(id, dir) { 101 | var sortItem = findSortItem(id); 102 | if (sortItem) { 103 | sortItem.dir = dir; 104 | } else { 105 | $scope.persistentState.sortOrder.push({ 106 | id: id, 107 | dir: dir 108 | }); 109 | } 110 | }; 111 | $scope.removeSort = function(id) { 112 | var idx = findSortItemIndex(id); 113 | if (idx !== -1) { 114 | $scope.persistentState.sortOrder.splice(idx, 1); 115 | } 116 | }; 117 | $scope.clearSort = function() { 118 | $scope.persistentState.sortOrder = []; 119 | }; 120 | // Checks if columns have any filter fileds 121 | $scope.hasFilterFields = function() { 122 | if (!$scope.columns) { 123 | return false; 124 | } 125 | for (var i = $scope.columns.length - 1; i >= 0; i--) { 126 | if (typeof $scope.columns[i].filter !== 'undefined') { 127 | return true; 128 | } 129 | } 130 | return false; 131 | }; 132 | // Clears search field for column, focus on input 133 | $scope.clearAndFocusSearch = function(columnId) { 134 | $scope.persistentState.searchTerms[columnId] = ''; 135 | $element.find('tr.ap-mesa-filter-row th.column-' + columnId + ' input').focus(); 136 | }; 137 | // Toggles column sorting 138 | $scope.toggleSort = function($event, column) { 139 | 140 | // check if even sortable 141 | if (!column.sort) { 142 | return; 143 | } 144 | 145 | // check for existing sort on this column 146 | var sortItem = findSortItem(column.id); 147 | 148 | if ( $event.shiftKey ) { 149 | // shift is down, ignore other columns 150 | // but toggle between three states 151 | if (sortItem) { 152 | if (sortItem.dir === '+') { 153 | sortItem.dir = '-'; 154 | } else if (sortItem.dir === '-') { 155 | $scope.removeSort(column.id); 156 | } 157 | } else { 158 | // Make ascending 159 | $scope.addSort(column.id, '+'); 160 | } 161 | 162 | } else { 163 | // shift is not down, disable other 164 | // columns but toggle two states 165 | var lastState = sortItem ? sortItem.dir : ''; 166 | $scope.clearSort(); 167 | if (lastState === '+') { 168 | $scope.addSort(column.id, '-'); 169 | } 170 | else { 171 | $scope.addSort(column.id, '+'); 172 | } 173 | 174 | } 175 | 176 | $scope.saveToStorage(); 177 | }; 178 | // Retrieve className for given sorting state 179 | $scope.getSortClass = function(sorting) { 180 | var classes = $scope.options.sortClasses; 181 | if (sorting === '+') { 182 | return classes[1]; 183 | } 184 | if (sorting === '-') { 185 | return classes[2]; 186 | } 187 | return classes[0]; 188 | }; 189 | $scope.setColumns = function(columns) { 190 | try { 191 | $scope.columns = columns; 192 | var lookup = $scope.transientState.columnLookup = {}; 193 | $scope.columns.forEach(function(column) { 194 | // formats 195 | var format = column.format; 196 | if (typeof format !== 'function') { 197 | if (typeof format === 'string') { 198 | if (typeof formats[format] === 'function') { 199 | column.format = formats[format]; 200 | } 201 | else { 202 | 203 | try { 204 | column.format = $filter(format); 205 | } catch (e) { 206 | delete column.format; 207 | $log.warn('format function reference in column(id=' + column.id + ') ' + 208 | 'was not found in built-in format functions or $filters. ' + 209 | 'format function given: "' + format + '". ' + 210 | 'Available built-ins: ' + Object.keys(formats).join(',') + '. ' + 211 | 'If you supplied a $filter, ensure it is available on this module'); 212 | } 213 | 214 | } 215 | } else { 216 | delete column.format; 217 | } 218 | } 219 | 220 | // async get 221 | if (!$scope.options.getData) { 222 | // sort 223 | var sort = column.sort; 224 | if (typeof sort !== 'function') { 225 | if (typeof sort === 'string') { 226 | if (typeof sorts[sort] === 'function') { 227 | column.sort = sorts[sort](column.key); 228 | } 229 | else { 230 | delete column.sort; 231 | $log.warn('sort function reference in column(id=' + column.id + ') ' + 232 | 'was not found in built-in sort functions. ' + 233 | 'sort function given: "' + sort + '". ' + 234 | 'Available built-ins: ' + Object.keys(sorts).join(',') + '. '); 235 | } 236 | } else { 237 | delete column.sort; 238 | } 239 | } 240 | // filter 241 | var filter = column.filter; 242 | if (typeof filter !== 'function') { 243 | if (typeof filter === 'string') { 244 | if (typeof filters[filter] === 'function') { 245 | column.filter = filters[filter]; 246 | } 247 | else { 248 | delete column.filter; 249 | $log.warn('filter function reference in column(id=' + column.id + ') ' + 250 | 'was not found in built-in filter functions. ' + 251 | 'filter function given: "' + filter + '". ' + 252 | 'Available built-ins: ' + Object.keys(filters).join(',') + '. '); 253 | } 254 | } else { 255 | delete column.filter; 256 | } 257 | } 258 | } 259 | 260 | // populate lookup 261 | lookup[column.id] = column; 262 | 263 | }); 264 | 265 | // check enabledColumns for validity 266 | if (angular.isArray($scope.enabledColumns)) { 267 | // if any of the ids in enabledColumns do not map to a column in the new column set... 268 | if ($scope.enabledColumns.some(function(columnId) { 269 | return !lookup[columnId]; 270 | })) { 271 | // ...unset the enabled columns 272 | $scope.enabledColumns = undefined; 273 | } 274 | } else { 275 | $scope.enabledColumns = $scope.columns.map(function(column) { 276 | return column.id; 277 | }); 278 | } 279 | } catch (e) { 280 | console.log(e.message); 281 | } 282 | }; 283 | 284 | $scope.startColumnResize = function($event, column) { 285 | 286 | // Stop default so text does not get selected 287 | $event.preventDefault(); 288 | $event.originalEvent.preventDefault(); 289 | $event.stopPropagation(); 290 | 291 | // init variable for new width 292 | var new_width = false; 293 | 294 | // store initial mouse position 295 | var initial_x = $event.pageX; 296 | 297 | // create marquee element 298 | var $m = $('
'); 299 | 300 | // append to th 301 | var $th = $($event.target).parent('th'); 302 | $th.append($m); 303 | 304 | // set initial marquee dimensions 305 | var initial_width = $th.outerWidth(); 306 | 307 | function mousemove(e) { 308 | // calculate changed width 309 | var current_x = e.pageX; 310 | var diff = current_x - initial_x; 311 | new_width = initial_width + diff; 312 | 313 | // update marquee dimensions 314 | $m.css('width', new_width + 'px'); 315 | } 316 | 317 | $m.css({ 318 | width: initial_width + 'px', 319 | height: $th.outerHeight() + 'px' 320 | }); 321 | 322 | // set mousemove listener 323 | $($window).on('mousemove', mousemove); 324 | 325 | // set mouseup/mouseout listeners 326 | $($window).one('mouseup', function(e) { 327 | e.stopPropagation(); 328 | // remove marquee, remove window mousemove listener 329 | $m.remove(); 330 | $($window).off('mousemove', mousemove); 331 | 332 | // set new width on th 333 | // if a new width was set 334 | if (new_width === false) { 335 | column.width = Math.max(initial_width, 0); 336 | } else { 337 | column.width = Math.max(new_width, CONSTANTS.minWidth); 338 | } 339 | 340 | $scope.$apply(); 341 | }); 342 | }; 343 | $scope.sortableOptions = { 344 | axis: 'x', 345 | handle: '.column-text', 346 | helper: 'clone', 347 | placeholder: 'ap-mesa-column-placeholder', 348 | distance: 5, 349 | update: function() { 350 | // use of $timeout req'd for this because the update event comes before 351 | // the model is updated! 352 | $timeout(function() { 353 | $scope.enabledColumns = $scope.enabledColumnObjects.map(function(column) { return column.id; }); 354 | }); 355 | } 356 | }; 357 | 358 | $scope.getActiveColCount = function() { 359 | var count = 0; 360 | $scope.columns.forEach(function(col) { 361 | if (!col.disabled) { 362 | count++; 363 | } 364 | }); 365 | return count; 366 | }; 367 | 368 | $scope.saveToStorage = function() { 369 | if (!$scope.storage) { 370 | return; 371 | } 372 | // init object to stringify/save 373 | var state = {}; 374 | 375 | // save state objects 376 | ['sortOrder', 'searchTerms'].forEach(function(prop) { 377 | state[prop] = $scope.persistentState[prop]; 378 | }); 379 | 380 | // save enabled columns (can't be in persistent state because it is a directive @Input) 381 | state.enabledColumns = $scope.enabledColumns; 382 | 383 | // save non-transient options 384 | state.options = {}; 385 | ['rowLimit', 'pagingStrategy', 'storageHash'].forEach(function(prop){ 386 | state.options[prop] = $scope.options[prop]; 387 | }); 388 | 389 | // Save to storage 390 | var valueToStore = $scope.options.stringifyStorage ? JSON.stringify(state) : state; 391 | $scope.storage.setItem($scope.storageKey, valueToStore); 392 | }; 393 | 394 | $scope.loadFromStorage = function() { 395 | 396 | var options = $scope.options; 397 | 398 | if (!$scope.storage) { 399 | return; 400 | } 401 | 402 | // Attempt to parse the storage 403 | var stateValue = $scope.storage.getItem($scope.storageKey); 404 | 405 | $q.when(stateValue).then(function(stateStringOrObject) { 406 | 407 | if (!stateStringOrObject) { 408 | console.warn('angularjs-table: loading from storage failed because storage.getItem did not return anything.'); 409 | return; 410 | } 411 | 412 | try { 413 | 414 | var state; 415 | if (options.stringifyStorage) { 416 | if (typeof stateStringOrObject !== 'string') { 417 | throw new TypeError('storage.getItem is expected to return a string if options.stringifyStorage is true.'); 418 | } 419 | state = JSON.parse(stateStringOrObject); 420 | } else if (angular.isObject(stateStringOrObject)) { 421 | state = stateStringOrObject; 422 | } else { 423 | throw new TypeError('storage.getItem is expected to return an object if options.stringifyStorage is false.'); 424 | } 425 | 426 | // if mimatched storage hash, stop loading from storage 427 | if (state.options.storageHash !== $scope.options.storageHash) { 428 | return; 429 | } 430 | 431 | // load state objects 432 | ['sortOrder', 'searchTerms'].forEach(function(prop){ 433 | $scope.persistentState[prop] = state[prop]; 434 | }); 435 | 436 | // load enabled columns list 437 | $scope.enabledColumns = state.enabledColumns; 438 | 439 | // load options 440 | ['rowLimit', 'pagingStrategy', 'storageHash'].forEach(function(prop) { 441 | $scope.options[prop] = state.options[prop]; 442 | }); 443 | 444 | } catch (e) { 445 | console.warn('angularjs-table: failed to load state from storage. ', e); 446 | } 447 | }, function(e) { 448 | console.warn('angularjs-table: storage.getItem failed: ', e); 449 | }); 450 | }; 451 | 452 | }]); 453 | -------------------------------------------------------------------------------- /src/directives/apMesaCell.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | angular.module('apMesa.directives.apMesaCell', [ 19 | 'apMesa.directives.apMesaSelector' 20 | ]) 21 | 22 | .directive('apMesaCell', function($compile) { 23 | 24 | function link(scope, element) { 25 | scope.$watch('column', function(column) { 26 | var cellMarkup = ''; 27 | if (column.template) { 28 | cellMarkup = column.template; 29 | } 30 | else if (column.templateUrl) { 31 | cellMarkup = '
'; 32 | } 33 | else if (column.selector === true) { 34 | cellMarkup = ''; 35 | } 36 | else if (column.ngFilter) { 37 | cellMarkup = '{{ row[column.key] | ' + column.ngFilter + ':row }}'; 38 | } 39 | else if (column.format) { 40 | var valueExpr = (scope.options !== undefined && {}.hasOwnProperty.call(scope.options, 'getter')) ? 'options.getter(column.key, row)' : 'row[column.key]'; 41 | cellMarkup = '{{ column.format(' + valueExpr + ', row, column, options) }}'; 42 | } 43 | else if(scope.options !== undefined && {}.hasOwnProperty.call(scope.options, 'getter')) { 44 | 45 | cellMarkup = '{{ options.getter(column.key, row) }}'; 46 | } 47 | else { 48 | cellMarkup = '{{ row[column.key] }}'; 49 | } 50 | element.html(cellMarkup); 51 | $compile(element.contents())(scope); 52 | }); 53 | } 54 | 55 | return { 56 | scope: true, 57 | link: link 58 | }; 59 | }); 60 | -------------------------------------------------------------------------------- /src/directives/apMesaDummyRows.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | /** 19 | * @ngdoc directive 20 | * @name apMesa.directive:apMesaDummyRows 21 | * @restrict A 22 | * @description inserts dummy s for non-rendered rows 23 | * @element tbody 24 | * @example 25 | **/ 26 | angular.module('apMesa.directives.apMesaDummyRows', []) 27 | .directive('apMesaDummyRows', function() { 28 | 29 | return { 30 | template: '', 31 | scope: true, 32 | link: function(scope, element, attrs) { 33 | 34 | scope.$on('angular-mesa:update-dummy-rows', function() { 35 | var offsetRange = scope.$eval(attrs.apMesaDummyRows); 36 | var rowsHeight = (offsetRange[1] - offsetRange[0]) * scope.rowHeight; 37 | for (var k in scope.transientState.expandedRows) { 38 | var kInt = parseInt(k); 39 | if (kInt >= offsetRange[0] && kInt < offsetRange[1]) { 40 | rowsHeight += scope.transientState.expandedRowHeights[k]; 41 | } 42 | } 43 | scope.dummyRowHeight = rowsHeight; 44 | }); 45 | 46 | } 47 | }; 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /src/directives/apMesaExpandable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('apMesa.directives.apMesaExpandable', []) 4 | .directive('apMesaExpandable', ['$compile', function($compile) { 5 | return { 6 | scope: false, 7 | link: function(scope, element, attrs) { 8 | scope.$watch('row', function() { 9 | var innerEl; 10 | if (scope.options.expandableTemplateUrl) { 11 | innerEl = angular.element('
'); 12 | } else if (scope.options.expandableTemplate) { 13 | innerEl = angular.element(scope.options.expandableTemplate); 14 | } else { 15 | return; 16 | } 17 | $compile(innerEl)(scope); 18 | element.html(''); 19 | element.append(innerEl); 20 | }); 21 | } 22 | }; 23 | }]); 24 | -------------------------------------------------------------------------------- /src/directives/apMesaPaginationCtrls.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('apMesa.directives.apMesaPaginationCtrls', []) 4 | .directive('apMesaPaginationCtrls', function($timeout) { 5 | 6 | return { 7 | templateUrl: 'src/templates/apMesaPaginationCtrls.tpl.html', 8 | scope: true, 9 | link: function(scope, element) { 10 | function updatePageLinks() { 11 | var pageLinks = []; 12 | var numPages = Math.ceil(scope.transientState.filterCount / scope.options.rowsPerPage); 13 | var currentPage = scope.transientState.pageOffset; 14 | var maxPageLinks = Math.max(5, scope.options.maxPageLinks); // must be a minimum of 5 max page links 15 | 16 | if (numPages <= maxPageLinks) { 17 | for (var i = 0; i < numPages; i++) { 18 | pageLinks.push({ 19 | gap: false, 20 | page: i, 21 | current: currentPage === i 22 | }); 23 | } 24 | } else if (currentPage < (maxPageLinks - 3)) { 25 | for (var i = 0; i < maxPageLinks - 2; i++) { 26 | pageLinks.push({ 27 | gap: false, 28 | page: i, 29 | current: currentPage === i 30 | }); 31 | } 32 | pageLinks.push({ 33 | gap: true, 34 | page: -1, 35 | current: false 36 | }, { 37 | gap: false, 38 | page: numPages - 1, 39 | current: false 40 | }); 41 | } else if (numPages - currentPage <= (maxPageLinks - 3)) { 42 | pageLinks.push({ 43 | gap: false, 44 | page: 0, 45 | current: false 46 | }, { 47 | gap: true, 48 | page: -1, 49 | current: false 50 | }); 51 | var startingPage = numPages - (maxPageLinks - 2); 52 | for (var i = startingPage; i < numPages; i++) { 53 | pageLinks.push({ 54 | gap: false, 55 | page: i, 56 | current: currentPage === i 57 | }); 58 | } 59 | } else { 60 | pageLinks.push({ 61 | gap: false, 62 | page: 0, 63 | current: false 64 | }, { 65 | gap: true, 66 | page: -1, 67 | current: false 68 | }); 69 | var remainingLinkCount = maxPageLinks - 4; 70 | for (var i = 0; remainingLinkCount > 0; i++) { 71 | var distance = i % 2 ? (i + 1)/2 : -(i / 2); 72 | var page = currentPage + distance; 73 | if (distance >= 0) { 74 | pageLinks.push({ 75 | gap: false, 76 | page: page, 77 | current: distance === 0 78 | }); 79 | } else { 80 | pageLinks.splice(2, 0, { 81 | gap: false, 82 | page: page, 83 | current: false 84 | }); 85 | } 86 | --remainingLinkCount; 87 | } 88 | pageLinks.push({ 89 | gap: true, 90 | page: -1, 91 | current: false 92 | }, { 93 | gap: false, 94 | page: numPages - 1, 95 | current: false 96 | }); 97 | } 98 | scope.pageLinks = pageLinks; 99 | scope.lastPage = numPages -1; 100 | } 101 | scope.$watch('transientState.filterCount', updatePageLinks); 102 | scope.$watch('options.rowsPerPage', updatePageLinks); 103 | scope.$watch('transientState.pageOffset', updatePageLinks); 104 | 105 | scope.goBack = function() { 106 | if (scope.transientState.pageOffset === 0) { 107 | return; 108 | } 109 | scope.transientState.pageOffset--; 110 | } 111 | 112 | scope.goForward = function() { 113 | if (scope.transientState.pageOffset === scope.lastPage) { 114 | return; 115 | } 116 | scope.transientState.pageOffset++; 117 | } 118 | } 119 | }; 120 | }); 121 | -------------------------------------------------------------------------------- /src/directives/apMesaRow.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('apMesa.directives.apMesaRow', ['apMesa.directives.apMesaCell']) 4 | .directive('apMesaRow', function($timeout) { 5 | return { 6 | template: '', 7 | scope: false, 8 | link: function(scope, element) { 9 | var index; 10 | 11 | if (scope.options.pagingStrategy === 'SCROLL') { 12 | index = scope.$index + scope.transientState.rowOffset; 13 | scope.rowIsExpanded = !!scope.transientState.expandedRows[index]; 14 | } else if (scope.options.pagingStrategy === 'PAGINATE') { 15 | scope.$watch('options.rowsPerPage', function(rowsPerPage) { 16 | index = scope.$index + (scope.transientState.pageOffset * rowsPerPage); 17 | scope.rowIsExpanded = !!scope.transientState.expandedRows[index]; 18 | }); 19 | } 20 | 21 | scope.$watch('transientState.expandedRows', function(nv, ov) { 22 | if (nv !== ov) { 23 | scope.rowIsExpanded = false; 24 | } 25 | }); 26 | 27 | scope.toggleRowExpand = function() { 28 | scope.transientState.expandedRows[index] = scope.rowIsExpanded = !scope.transientState.expandedRows[index]; 29 | if (!scope.transientState.expandedRows[index]) { 30 | delete scope.transientState.expandedRows[index]; 31 | delete scope.transientState.expandedRowHeights[index]; 32 | } else { 33 | scope.refreshExpandedHeight(false); 34 | } 35 | }; 36 | scope.refreshExpandedHeight = function(fromTemplate) { 37 | $timeout(function() { 38 | var newHeight = element.next('tr.ap-mesa-expand-panel').height(); 39 | scope.transientState.expandedRowHeights[index] = newHeight; 40 | }); 41 | }; 42 | } 43 | }; 44 | }); 45 | -------------------------------------------------------------------------------- /src/directives/apMesaRows.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | angular.module('apMesa.directives.apMesaRows',[ 19 | 'apMesa.directives.apMesaRow', 20 | 'apMesa.filters.apMesaRowFilter', 21 | 'apMesa.filters.apMesaRowSorter', 22 | 'apMesa.services.apMesaDebounce' 23 | ]) 24 | 25 | .directive('apMesaRows', ['$filter', '$timeout', 'apMesaDebounce', '$rootScope', function($filter, $timeout, debounce, $rootScope) { 26 | 27 | var tableRowFilter = $filter('apMesaRowFilter'); 28 | var tableRowSorter = $filter('apMesaRowSorter'); 29 | var limitTo = $filter('limitTo'); 30 | 31 | /** 32 | * Updates the visible_rows array on the scope synchronously. 33 | * @param {ng.IScope} scope The scope of the particular apMesaRows instance. 34 | * @return {void} 35 | */ 36 | function updateVisibleRows(scope) { 37 | 38 | // sanity check 39 | if (!scope.rows || !scope.enabledColumnObjects) { 40 | return []; 41 | } 42 | 43 | // scope.rows 44 | var visible_rows, idx; 45 | 46 | // filter rows 47 | visible_rows = tableRowFilter(scope.rows, scope.enabledColumnObjects, scope.persistentState, scope.transientState, scope.options); 48 | 49 | // sort rows 50 | visible_rows = tableRowSorter(visible_rows, scope.enabledColumnObjects, scope.persistentState.sortOrder, scope.options, scope.transientState); 51 | 52 | // limit rows 53 | if (scope.options.pagingStrategy === 'SCROLL') { 54 | visible_rows = limitTo(visible_rows, Math.floor(scope.transientState.rowOffset) - scope.transientState.filterCount); 55 | visible_rows = limitTo(visible_rows, scope.persistentState.rowLimit + Math.ceil(scope.transientState.rowOffset % 1)); 56 | idx = scope.transientState.rowOffset; 57 | } else if (scope.options.pagingStrategy === 'PAGINATE') { 58 | var pagedRowOffset = scope.transientState.pageOffset * scope.persistentState.rowLimit; 59 | visible_rows = visible_rows.slice(pagedRowOffset, pagedRowOffset + scope.persistentState.rowLimit); 60 | idx = pagedRowOffset; 61 | } 62 | 63 | // add index to each row 64 | visible_rows.forEach(function(row) { 65 | row.$$$index = idx++; 66 | }); 67 | 68 | scope.visible_rows = visible_rows; 69 | scope.$broadcast('angular-mesa:update-dummy-rows'); 70 | } 71 | 72 | /** 73 | * Updates the visible_rows array on the scope asynchronously, using the options.getData function (when present). 74 | * This gets passed to debounce in the link function. 75 | * @param {ng.IScope} scope The scope of the particular apMesaRows instance 76 | * @return {ng.IPromise} Returns the promise of the request 77 | */ 78 | function UpdateVisibleRowsAsync(scope) { 79 | // get offset 80 | var offset; 81 | if (scope.options.pagingStrategy === 'SCROLL') { 82 | offset = scope.transientState.rowOffset; 83 | } else if (scope.options.pagingStrategy === 'PAGINATE') { 84 | offset = scope.transientState.pageOffset * scope.persistentState.rowLimit; 85 | } 86 | 87 | // get active filter 88 | var searchTerms = scope.persistentState.searchTerms; 89 | var activeFilters = scope.enabledColumnObjects 90 | .filter(function(column) { 91 | return !! searchTerms[column.id]; 92 | }) 93 | .map(function(column) { 94 | return { column: column, value: searchTerms[column.id] }; 95 | }); 96 | 97 | // get active sorts 98 | var enabledColumnLookup = {}; 99 | scope.enabledColumns.forEach(function(id) { 100 | enabledColumnLookup[id] = true; 101 | }); 102 | var activeSorts = scope.persistentState.sortOrder 103 | .filter(function(sortItem) { 104 | return enabledColumnLookup[sortItem.id]; 105 | }) 106 | .map(function(sortItem) { 107 | return { 108 | column: scope.transientState.columnLookup[sortItem.id], 109 | direction: sortItem.dir === '+' ? 'ASC' : 'DESC' 110 | }; 111 | }); 112 | 113 | scope.transientState.loadingError = false; 114 | scope.api.setLoading(true); 115 | var getDataPromise = scope.transientState.getDataPromise = scope.options.getData( 116 | offset, 117 | scope.persistentState.rowLimit, 118 | activeFilters, 119 | activeSorts 120 | ).then(function(res) { 121 | if (getDataPromise !== scope.transientState.getDataPromise) { 122 | // new request made, cancelling this one 123 | return; 124 | } 125 | var total = res.total; 126 | var rows = res.rows; 127 | var i = offset; 128 | scope.transientState.rowOffset = offset; 129 | scope.transientState.filterCount = total; 130 | scope.visible_rows = rows; 131 | rows.forEach(function(row) { 132 | row.$$$index = i++; 133 | }); 134 | scope.transientState.getDataPromise = null; 135 | scope.api.setLoading(false); 136 | scope.getDataError = undefined; 137 | scope.$emit('angular-mesa:update-dummy-rows'); 138 | }, function(e) { 139 | scope.transientState.getDataPromise = null; 140 | scope.transientState.loadingError = true; 141 | scope.getDataError = e; 142 | scope.api.setLoading(false); 143 | }); 144 | } 145 | 146 | function link(scope) { 147 | 148 | var updateVisibleRowsAsync = debounce(UpdateVisibleRowsAsync, 200, { leading: false, trailing: true }) 149 | 150 | var updateHandler = function(newValue, oldValue) { 151 | if (newValue === oldValue) { 152 | return; 153 | } 154 | if (!scope.options.getData) { 155 | updateVisibleRows(scope); 156 | } else { 157 | updateVisibleRowsAsync(scope); 158 | } 159 | 160 | scope.transientState.expandedRows = {}; 161 | }; 162 | 163 | var updateHandlerWithoutClearingCollapsed = function(newValue, oldValue) { 164 | if (newValue === oldValue) { 165 | return; 166 | } 167 | if (!scope.options.getData) { 168 | updateVisibleRows(scope); 169 | } else { 170 | updateVisibleRowsAsync(scope); 171 | } 172 | 173 | }; 174 | 175 | // Watchers that trigger updates to visible rows 176 | scope.$watch('persistentState.searchTerms', function(nv, ov) { 177 | if (!angular.equals(nv, ov)) { 178 | scope.resetOffset(); 179 | } 180 | updateHandler(nv, ov); 181 | }, true); 182 | scope.$watch('persistentState.sortOrder', function(nv, ov) { 183 | if (!angular.equals(nv, ov)) { 184 | scope.resetOffset(); 185 | } 186 | updateHandler(nv, ov); 187 | }, true); 188 | scope.$watch('transientState.rowOffset', function(nv, ov) { 189 | if (scope.options.pagingStrategy === 'SCROLL') { 190 | updateHandlerWithoutClearingCollapsed(nv, ov); 191 | } 192 | }); 193 | scope.$watch('persistentState.rowLimit', function(nv, ov) { 194 | updateHandlerWithoutClearingCollapsed(nv, ov); 195 | }); 196 | scope.$watch('transientState.pageOffset', function(nv, ov) { 197 | updateHandlerWithoutClearingCollapsed(nv, ov); 198 | }); 199 | scope.$watch('transientState.filterCount', function(nv, ov) { 200 | if (!scope.options.getData) { 201 | updateHandler(nv, ov); 202 | } 203 | }); 204 | scope.$watch('rows', function(newRows) { 205 | if (angular.isArray(newRows)) { 206 | updateHandler(true, false); 207 | } 208 | }); 209 | scope.$watch('enabledColumnObjects', function(nv, ov) { 210 | updateHandler(nv, ov); 211 | }); 212 | scope.$watch('options.getData', function(getData) { 213 | if (angular.isFunction(getData)) { 214 | updateHandler(true, false); 215 | } 216 | }); 217 | scope.$on('apMesa:forceRefresh', function() { 218 | updateHandler(true, false); 219 | }); 220 | } 221 | 222 | return { 223 | restrict: 'A', 224 | templateUrl: 'src/templates/apMesaRows.tpl.html', 225 | compile: function(tElement, tAttrs) { 226 | var tr = tElement.find('tr[ng-repeat-start]'); 227 | var repeatString = tr.attr('ng-repeat-start'); 228 | repeatString += tAttrs.trackBy ? ' track by row[options.trackBy]' : ' track by row.$$$index'; 229 | tr.attr('ng-repeat-start', repeatString); 230 | if (tAttrs.onRowClick) { 231 | tElement.find('tr[ng-repeat-start]').attr('ng-click', tAttrs.onRowClick) 232 | } 233 | return link; 234 | } 235 | }; 236 | }]); 237 | -------------------------------------------------------------------------------- /src/directives/apMesaSelector.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | angular.module('apMesa.directives.apMesaSelector', []) 19 | 20 | .directive('apMesaSelector', function() { 21 | return { 22 | restrict: 'A', 23 | scope: false, 24 | link: function postLink(scope, element) { 25 | var selected = scope.selected; 26 | var row = scope.row; 27 | var column = scope.column; 28 | element.on('click', function() { 29 | 30 | // Retrieve position in selected list 31 | var idx = selected.indexOf(column.selectObject ? row : row[column.key]); 32 | 33 | // it is selected, deselect it: 34 | if (idx >= 0) { 35 | selected.splice(idx,1); 36 | } 37 | 38 | // it is not selected, push to list 39 | else { 40 | selected.push(column.selectObject ? row : row[column.key]); 41 | } 42 | scope.$apply(); 43 | }); 44 | } 45 | }; 46 | }); 47 | -------------------------------------------------------------------------------- /src/directives/apMesaStatusDisplay.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('apMesa.directives.apMesaStatusDisplay', []) 4 | 5 | .directive('apMesaStatusDisplay', function() { 6 | return { 7 | replace: true, 8 | templateUrl: 'src/templates/apMesaStatusDisplay.tpl.html', 9 | }; 10 | }); 11 | -------------------------------------------------------------------------------- /src/directives/apMesaThTitle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('apMesa.directives.apMesaThTitle', []) 4 | .directive('apMesaThTitle', function($compile) { 5 | function link(scope, element) { 6 | var column = scope.column; 7 | var template = '{{ column.id }}'; 8 | if (angular.isString(column.labelTemplateUrl)) { 9 | template = ''; 10 | } else if (angular.isString(column.labelTemplate)) { 11 | template = '' + column.labelTemplate + ''; 12 | } else if (angular.isString(column.label)) { 13 | template = '{{ column.label }}'; 14 | } 15 | element.html(template); 16 | $compile(element.contents())(scope); 17 | } 18 | return { link: link }; 19 | }); 20 | -------------------------------------------------------------------------------- /src/filters/apMesaRowFilter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | angular.module('apMesa.filters.apMesaRowFilter',[ 19 | 'apMesa.services.apMesaFilterFunctions' 20 | ]) 21 | 22 | .filter('apMesaRowFilter', ['apMesaFilterFunctions', '$log', function(tableFilterFunctions, $log) { 23 | return function tableRowFilter(rows, columns, persistentState, transientState, options) { 24 | 25 | var enabledFilterColumns, result = rows; 26 | 27 | // gather enabled filter functions 28 | enabledFilterColumns = columns.filter(function(column) { 29 | // check search term 30 | var term = persistentState.searchTerms[column.id]; 31 | if (typeof term === 'string') { 32 | 33 | // filter empty strings and whitespace 34 | if (!term.trim()) { 35 | return false; 36 | } 37 | 38 | // check search filter function 39 | if (typeof column.filter === 'function') { 40 | return true; 41 | } 42 | // not a function, check for predefined filter function 43 | var predefined = tableFilterFunctions[column.filter]; 44 | if (typeof predefined === 'function') { 45 | column.filter = predefined; 46 | return true; 47 | } 48 | $log.warn('apMesa: The filter function "'+column.filter+'" ' + 49 | 'specified by column(id='+column.id+').filter ' + 50 | 'was not found in predefined tableFilterFunctions. ' + 51 | 'Available filters: "'+Object.keys(tableFilterFunctions).join('","')+'"'); 52 | } 53 | return false; 54 | }); 55 | 56 | // loop through rows and filter on every enabled function 57 | if (enabledFilterColumns.length) { 58 | result = rows.filter(function(row) { 59 | for (var i = enabledFilterColumns.length - 1; i >= 0; i--) { 60 | var col = enabledFilterColumns[i]; 61 | var filter = col.filter; 62 | var term = persistentState.searchTerms[col.id]; 63 | var value = (options !== undefined && {}.hasOwnProperty.call(options, 'getter'))? options.getter(col.key, row):row[col.key]; 64 | var computedValue = typeof col.format === 'function' ? col.format(value, row, col, options) : value; 65 | if (!filter(term, value, computedValue, row, col, options)) { 66 | return false; 67 | } 68 | } 69 | return true; 70 | }); 71 | } 72 | transientState.filterCount = result.length; 73 | return result; 74 | }; 75 | }]); 76 | -------------------------------------------------------------------------------- /src/filters/apMesaRowSorter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | angular.module('apMesa.filters.apMesaRowSorter', []) 19 | 20 | .filter('apMesaRowSorter', function() { 21 | return function tableRowSorter(rows, columns, sortOrder, options, transientState) { 22 | if (!sortOrder.length) { 23 | return rows; 24 | } 25 | var arrayCopy = rows.slice(); 26 | var enabledColumns = {}; 27 | columns.forEach(function(column) { 28 | enabledColumns[column.id] = true; 29 | }); 30 | // js sort doesn't work as expected because it rearranges the equal elements 31 | // so we will arrange elements only if they are different, based on the element index 32 | var sortArray = arrayCopy.map(function (data, index) { 33 | return { index: index, data: data }; 34 | }); 35 | 36 | sortArray.sort(function (a, b) { 37 | for (var i = 0; i < sortOrder.length; i++) { 38 | var sortItem = sortOrder[i]; 39 | if (!enabledColumns[sortItem.id]) { 40 | continue; 41 | } 42 | var column = transientState.columnLookup[sortItem.id]; 43 | var dir = sortItem.dir; 44 | if (column && column.sort) { 45 | var fn = column.sort; 46 | var result = dir === '+' ? fn(a.data, b.data, options, column) : fn(b.data, a.data, options, column); 47 | if (result !== 0) { 48 | return result; 49 | } 50 | } 51 | } 52 | return a.index - b.index; 53 | }); 54 | 55 | return sortArray.map(function (value) { 56 | return value.data; 57 | }); 58 | }; 59 | }); 60 | -------------------------------------------------------------------------------- /src/services/apMesaDebounce.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** _.debounce, modified to use $timeout instead of setTimeout */ 4 | angular.module('apMesa.services.apMesaDebounce', []) 5 | 6 | .factory('apMesaDebounce', ['$timeout', function($timeout) { 7 | /** 8 | * lodash (Custom Build) 9 | * Build: `lodash modularize exports="npm" -o ./` 10 | * Copyright jQuery Foundation and other contributors 11 | * Released under MIT license 12 | * Based on Underscore.js 1.8.3 13 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 14 | */ 15 | 16 | /** Used as the `TypeError` message for "Functions" methods. */ 17 | var FUNC_ERROR_TEXT = 'Expected a function'; 18 | 19 | /** Used as references for various `Number` constants. */ 20 | var NAN = 0 / 0; 21 | 22 | /** `Object#toString` result references. */ 23 | var symbolTag = '[object Symbol]'; 24 | 25 | /** Used to match leading and trailing whitespace. */ 26 | var reTrim = /^\s+|\s+$/g; 27 | 28 | /** Used to detect bad signed hexadecimal string values. */ 29 | var reIsBadHex = /^[-+]0x[0-9a-f]+$/i; 30 | 31 | /** Used to detect binary string values. */ 32 | var reIsBinary = /^0b[01]+$/i; 33 | 34 | /** Used to detect octal string values. */ 35 | var reIsOctal = /^0o[0-7]+$/i; 36 | 37 | /** Built-in method references without a dependency on `root`. */ 38 | var freeParseInt = parseInt; 39 | 40 | /** Detect free variable `global` from Node.js. */ 41 | var freeGlobal = typeof global == 'object' && global && global.Object === Object && global; 42 | 43 | /** Detect free variable `self`. */ 44 | var freeSelf = typeof self == 'object' && self && self.Object === Object && self; 45 | 46 | /** Used as a reference to the global object. */ 47 | var root = freeGlobal || freeSelf || Function('return this')(); 48 | 49 | /** Used for built-in method references. */ 50 | var objectProto = Object.prototype; 51 | 52 | /** 53 | * Used to resolve the 54 | * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) 55 | * of values. 56 | */ 57 | var objectToString = objectProto.toString; 58 | 59 | /* Built-in method references for those with the same name as other `lodash` methods. */ 60 | var nativeMax = Math.max, 61 | nativeMin = Math.min; 62 | 63 | /** 64 | * Gets the timestamp of the number of milliseconds that have elapsed since 65 | * the Unix epoch (1 January 1970 00:00:00 UTC). 66 | * 67 | * @static 68 | * @memberOf _ 69 | * @since 2.4.0 70 | * @category Date 71 | * @returns {number} Returns the timestamp. 72 | * @example 73 | * 74 | * _.defer(function(stamp) { 75 | * console.log(_.now() - stamp); 76 | * }, _.now()); 77 | * // => Logs the number of milliseconds it took for the deferred invocation. 78 | */ 79 | var now = function() { 80 | return root.Date.now(); 81 | }; 82 | 83 | /** 84 | * Creates a debounced function that delays invoking `func` until after `wait` 85 | * milliseconds have elapsed since the last time the debounced function was 86 | * invoked. The debounced function comes with a `cancel` method to cancel 87 | * delayed `func` invocations and a `flush` method to immediately invoke them. 88 | * Provide `options` to indicate whether `func` should be invoked on the 89 | * leading and/or trailing edge of the `wait` timeout. The `func` is invoked 90 | * with the last arguments provided to the debounced function. Subsequent 91 | * calls to the debounced function return the result of the last `func` 92 | * invocation. 93 | * 94 | * **Note:** If `leading` and `trailing` options are `true`, `func` is 95 | * invoked on the trailing edge of the timeout only if the debounced function 96 | * is invoked more than once during the `wait` timeout. 97 | * 98 | * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred 99 | * until to the next tick, similar to `$timeout` with a timeout of `0`. 100 | * 101 | * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) 102 | * for details over the differences between `_.debounce` and `_.throttle`. 103 | * 104 | * @static 105 | * @memberOf _ 106 | * @since 0.1.0 107 | * @category Function 108 | * @param {Function} func The function to debounce. 109 | * @param {number} [wait=0] The number of milliseconds to delay. 110 | * @param {Object} [options={}] The options object. 111 | * @param {boolean} [options.leading=false] 112 | * Specify invoking on the leading edge of the timeout. 113 | * @param {number} [options.maxWait] 114 | * The maximum time `func` is allowed to be delayed before it's invoked. 115 | * @param {boolean} [options.trailing=true] 116 | * Specify invoking on the trailing edge of the timeout. 117 | * @returns {Function} Returns the new debounced function. 118 | * @example 119 | * 120 | * // Avoid costly calculations while the window size is in flux. 121 | * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); 122 | * 123 | * // Invoke `sendMail` when clicked, debouncing subsequent calls. 124 | * jQuery(element).on('click', _.debounce(sendMail, 300, { 125 | * 'leading': true, 126 | * 'trailing': false 127 | * })); 128 | * 129 | * // Ensure `batchLog` is invoked once after 1 second of debounced calls. 130 | * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 }); 131 | * var source = new EventSource('/stream'); 132 | * jQuery(source).on('message', debounced); 133 | * 134 | * // Cancel the trailing debounced invocation. 135 | * jQuery(window).on('popstate', debounced.cancel); 136 | */ 137 | function debounce(func, wait, options) { 138 | var lastArgs, 139 | lastThis, 140 | maxWait, 141 | result, 142 | timerId, 143 | lastCallTime, 144 | lastInvokeTime = 0, 145 | leading = false, 146 | maxing = false, 147 | trailing = true; 148 | 149 | if (typeof func != 'function') { 150 | throw new TypeError(FUNC_ERROR_TEXT); 151 | } 152 | wait = toNumber(wait) || 0; 153 | if (isObject(options)) { 154 | leading = !!options.leading; 155 | maxing = 'maxWait' in options; 156 | maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait; 157 | trailing = 'trailing' in options ? !!options.trailing : trailing; 158 | } 159 | 160 | function invokeFunc(time) { 161 | var args = lastArgs, 162 | thisArg = lastThis; 163 | 164 | lastArgs = lastThis = undefined; 165 | lastInvokeTime = time; 166 | result = func.apply(thisArg, args); 167 | return result; 168 | } 169 | 170 | function leadingEdge(time) { 171 | // Reset any `maxWait` timer. 172 | lastInvokeTime = time; 173 | // Start the timer for the trailing edge. 174 | timerId = $timeout(timerExpired, wait); 175 | // Invoke the leading edge. 176 | return leading ? invokeFunc(time) : result; 177 | } 178 | 179 | function remainingWait(time) { 180 | var timeSinceLastCall = time - lastCallTime, 181 | timeSinceLastInvoke = time - lastInvokeTime, 182 | result = wait - timeSinceLastCall; 183 | 184 | return maxing ? nativeMin(result, maxWait - timeSinceLastInvoke) : result; 185 | } 186 | 187 | function shouldInvoke(time) { 188 | var timeSinceLastCall = time - lastCallTime, 189 | timeSinceLastInvoke = time - lastInvokeTime; 190 | 191 | // Either this is the first call, activity has stopped and we're at the 192 | // trailing edge, the system time has gone backwards and we're treating 193 | // it as the trailing edge, or we've hit the `maxWait` limit. 194 | return (lastCallTime === undefined || (timeSinceLastCall >= wait) || 195 | (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)); 196 | } 197 | 198 | function timerExpired() { 199 | var time = now(); 200 | if (shouldInvoke(time)) { 201 | return trailingEdge(time); 202 | } 203 | // Restart the timer. 204 | timerId = $timeout(timerExpired, remainingWait(time)); 205 | } 206 | 207 | function trailingEdge(time) { 208 | timerId = undefined; 209 | 210 | // Only invoke if we have `lastArgs` which means `func` has been 211 | // debounced at least once. 212 | if (trailing && lastArgs) { 213 | return invokeFunc(time); 214 | } 215 | lastArgs = lastThis = undefined; 216 | return result; 217 | } 218 | 219 | function cancel() { 220 | if (timerId !== undefined) { 221 | $timeout.cancel(timerId); 222 | } 223 | lastInvokeTime = 0; 224 | lastArgs = lastCallTime = lastThis = timerId = undefined; 225 | } 226 | 227 | function flush() { 228 | return timerId === undefined ? result : trailingEdge(now()); 229 | } 230 | 231 | function debounced() { 232 | var time = now(), 233 | isInvoking = shouldInvoke(time); 234 | 235 | lastArgs = arguments; 236 | lastThis = this; 237 | lastCallTime = time; 238 | 239 | if (isInvoking) { 240 | if (timerId === undefined) { 241 | return leadingEdge(lastCallTime); 242 | } 243 | if (maxing) { 244 | // Handle invocations in a tight loop. 245 | timerId = $timeout(timerExpired, wait); 246 | return invokeFunc(lastCallTime); 247 | } 248 | } 249 | if (timerId === undefined) { 250 | timerId = $timeout(timerExpired, wait); 251 | } 252 | return result; 253 | } 254 | debounced.cancel = cancel; 255 | debounced.flush = flush; 256 | return debounced; 257 | } 258 | 259 | /** 260 | * Checks if `value` is the 261 | * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) 262 | * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) 263 | * 264 | * @static 265 | * @memberOf _ 266 | * @since 0.1.0 267 | * @category Lang 268 | * @param {*} value The value to check. 269 | * @returns {boolean} Returns `true` if `value` is an object, else `false`. 270 | * @example 271 | * 272 | * _.isObject({}); 273 | * // => true 274 | * 275 | * _.isObject([1, 2, 3]); 276 | * // => true 277 | * 278 | * _.isObject(_.noop); 279 | * // => true 280 | * 281 | * _.isObject(null); 282 | * // => false 283 | */ 284 | function isObject(value) { 285 | var type = typeof value; 286 | return !!value && (type == 'object' || type == 'function'); 287 | } 288 | 289 | /** 290 | * Checks if `value` is object-like. A value is object-like if it's not `null` 291 | * and has a `typeof` result of "object". 292 | * 293 | * @static 294 | * @memberOf _ 295 | * @since 4.0.0 296 | * @category Lang 297 | * @param {*} value The value to check. 298 | * @returns {boolean} Returns `true` if `value` is object-like, else `false`. 299 | * @example 300 | * 301 | * _.isObjectLike({}); 302 | * // => true 303 | * 304 | * _.isObjectLike([1, 2, 3]); 305 | * // => true 306 | * 307 | * _.isObjectLike(_.noop); 308 | * // => false 309 | * 310 | * _.isObjectLike(null); 311 | * // => false 312 | */ 313 | function isObjectLike(value) { 314 | return !!value && typeof value == 'object'; 315 | } 316 | 317 | /** 318 | * Checks if `value` is classified as a `Symbol` primitive or object. 319 | * 320 | * @static 321 | * @memberOf _ 322 | * @since 4.0.0 323 | * @category Lang 324 | * @param {*} value The value to check. 325 | * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. 326 | * @example 327 | * 328 | * _.isSymbol(Symbol.iterator); 329 | * // => true 330 | * 331 | * _.isSymbol('abc'); 332 | * // => false 333 | */ 334 | function isSymbol(value) { 335 | return typeof value == 'symbol' || 336 | (isObjectLike(value) && objectToString.call(value) == symbolTag); 337 | } 338 | 339 | /** 340 | * Converts `value` to a number. 341 | * 342 | * @static 343 | * @memberOf _ 344 | * @since 4.0.0 345 | * @category Lang 346 | * @param {*} value The value to process. 347 | * @returns {number} Returns the number. 348 | * @example 349 | * 350 | * _.toNumber(3.2); 351 | * // => 3.2 352 | * 353 | * _.toNumber(Number.MIN_VALUE); 354 | * // => 5e-324 355 | * 356 | * _.toNumber(Infinity); 357 | * // => Infinity 358 | * 359 | * _.toNumber('3.2'); 360 | * // => 3.2 361 | */ 362 | function toNumber(value) { 363 | if (typeof value == 'number') { 364 | return value; 365 | } 366 | if (isSymbol(value)) { 367 | return NAN; 368 | } 369 | if (isObject(value)) { 370 | var other = typeof value.valueOf == 'function' ? value.valueOf() : value; 371 | value = isObject(other) ? (other + '') : other; 372 | } 373 | if (typeof value != 'string') { 374 | return value === 0 ? value : +value; 375 | } 376 | value = value.replace(reTrim, ''); 377 | var isBinary = reIsBinary.test(value); 378 | return (isBinary || reIsOctal.test(value)) 379 | ? freeParseInt(value.slice(2), isBinary ? 2 : 8) 380 | : (reIsBadHex.test(value) ? NAN : +value); 381 | } 382 | 383 | return debounce; 384 | 385 | }]); 386 | -------------------------------------------------------------------------------- /src/services/apMesaFilterFunctions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | angular.module('apMesa.services.apMesaFilterFunctions', []) 19 | 20 | .service('apMesaFilterFunctions', function() { 21 | 22 | function like(term, value) { 23 | term = term.toLowerCase().trim(); 24 | value = String(value).toLowerCase(); 25 | var first = term[0]; 26 | 27 | // negate 28 | if (first === '!') { 29 | term = term.substr(1); 30 | if (term === '') { 31 | return true; 32 | } 33 | return value.indexOf(term) === -1; 34 | } 35 | 36 | // strict 37 | if (first === '=') { 38 | term = term.substr(1); 39 | return term === value.trim(); 40 | } 41 | 42 | // remove escaping backslashes 43 | term = term.replace('\\!', '!'); 44 | term = term.replace('\\=', '='); 45 | 46 | return value.indexOf(term) !== -1; 47 | } 48 | 49 | function likeFormatted(term, value, computedValue, row) { 50 | return like(term,computedValue,computedValue, row); 51 | } 52 | like.placeholder = likeFormatted.placeholder = 'string search'; 53 | like.title = likeFormatted.title = 'Search by text, eg. "foo". Use "!" to exclude and "=" to match exact text, e.g. "!bar" or "=baz".'; 54 | 55 | function number(term, value) { 56 | value = parseFloat(value); 57 | term = term.trim(); 58 | var first_two = term.substr(0,2); 59 | var first_char = term[0]; 60 | var against_1 = term.substr(1)*1; 61 | var against_2 = term.substr(2)*1; 62 | if ( first_two === '<=' ) { 63 | return value <= against_2 ; 64 | } 65 | if ( first_two === '>=' ) { 66 | return value >= against_2 ; 67 | } 68 | if ( first_char === '<' ) { 69 | return value < against_1 ; 70 | } 71 | if ( first_char === '>' ) { 72 | return value > against_1 ; 73 | } 74 | if ( first_char === '~' ) { 75 | return Math.round(value) === against_1 ; 76 | } 77 | if ( first_char === '=' ) { 78 | return against_1 === value ; 79 | } 80 | return value.toString().indexOf(term.toString()) > -1 ; 81 | } 82 | function numberFormatted(term, value, computedValue) { 83 | return number(term, computedValue); 84 | } 85 | number.placeholder = numberFormatted.placeholder = 'number search'; 86 | number.title = numberFormatted.title = 'Search by number, e.g. "123". Optionally use comparator expressions like ">=10" or "<1000". Use "~" for approx. int values, eg. "~3" will match "3.2"'; 87 | 88 | 89 | var unitmap = {}; 90 | unitmap.second = unitmap.sec = unitmap.s = 1000; 91 | unitmap.minute = unitmap.min = unitmap.m = unitmap.second * 60; 92 | unitmap.hour = unitmap.hr = unitmap.h = unitmap.minute * 60; 93 | unitmap.day = unitmap.d = unitmap.hour * 24; 94 | unitmap.week = unitmap.wk = unitmap.w = unitmap.day * 7; 95 | unitmap.month = unitmap.week * 4; 96 | unitmap.year = unitmap.yr = unitmap.y = unitmap.day * 365; 97 | 98 | var clauseExp = /(\d+(?:\.\d+)?)\s*([a-z]+)/; 99 | function parseDateFilter(string) { 100 | 101 | // split on clauses (if any) 102 | var clauses = string.trim().split(','); 103 | var total = 0; 104 | // parse each clause 105 | for (var i = 0; i < clauses.length; i++) { 106 | var clause = clauses[i].trim(); 107 | var terms = clauseExp.exec(clause); 108 | if (!terms) { 109 | continue; 110 | } 111 | var count = terms[1]*1; 112 | var unit = terms[2].replace(/s$/, ''); 113 | if (! unitmap.hasOwnProperty(unit) ) { 114 | continue; 115 | } 116 | total += count * unitmap[unit]; 117 | } 118 | return total; 119 | 120 | } 121 | function date(term, value) { 122 | // today 123 | // yesterday 124 | // 1 day ago 125 | // 2 days ago 126 | 127 | // < 1 day ago 128 | // < 10 minutes ago 129 | // < 10 min ago 130 | // < 10 minutes, 50 seconds ago 131 | // > 10 min, 30 sec ago 132 | // > 2 days ago 133 | // >= 1 day ago 134 | term = term.trim(); 135 | if (!term) { 136 | return true; 137 | } 138 | value *= 1; 139 | var nowDate = new Date(); 140 | var now = (+nowDate); 141 | var first_char = term[0]; 142 | var other_chars = (term.substr(1)).trim(); 143 | var lowerbound, upperbound; 144 | if ( first_char === '<' ) { 145 | lowerbound = now - parseDateFilter(other_chars); 146 | return value > lowerbound; 147 | } 148 | if ( first_char === '>' ) { 149 | upperbound = now - parseDateFilter(other_chars); 150 | return value < upperbound; 151 | } 152 | 153 | if ( term === 'today') { 154 | return new Date(value).toDateString() === nowDate.toDateString(); 155 | } 156 | 157 | if ( term === 'yesterday') { 158 | return new Date(value).toDateString() === new Date(now - unitmap.d).toDateString(); 159 | } 160 | 161 | var supposedDate = new Date(term); 162 | if (!isNaN(supposedDate)) { 163 | return new Date(value).toDateString() === supposedDate.toDateString(); 164 | } 165 | 166 | return false; 167 | } 168 | date.placeholder = 'date search'; 169 | date.title = 'Search by date. Enter a date string (RFC2822 or ISO 8601 date). You can also type "today", "yesterday", "> 2 days ago", "< 1 day 2 hours ago", etc.'; 170 | 171 | return { 172 | like: like, 173 | likeFormatted: likeFormatted, 174 | number: number, 175 | numberFormatted: numberFormatted, 176 | date: date 177 | }; 178 | }); 179 | -------------------------------------------------------------------------------- /src/services/apMesaFormatFunctions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | angular.module('apMesa.services.apMesaFormatFunctions', []) 19 | 20 | .service('apMesaFormatFunctions', function() { 21 | // TODO: add some default format functions 22 | return {}; 23 | }); 24 | -------------------------------------------------------------------------------- /src/services/apMesaSortFunctions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | angular.module('apMesa.services.apMesaSortFunctions',[]) 19 | 20 | .service('apMesaSortFunctions', function() { 21 | return { 22 | number: function(field){ 23 | return function(row1,row2,options) { 24 | var val1, val2; 25 | if (options !== undefined && {}.hasOwnProperty.call(options, 'getter')) { 26 | val1 = options.getter(field, row1); 27 | val2 = options.getter(field, row2); 28 | } 29 | else { 30 | val1 = row1[field]; 31 | val2 = row2[field]; 32 | } 33 | return val1*1 - val2*1; 34 | }; 35 | }, 36 | string: function(field){ 37 | return function(row1,row2,options) { 38 | var val1, val2; 39 | if (options !== undefined && {}.hasOwnProperty.call(options, 'getter')) { 40 | val1 = options.getter(field, row1); 41 | val2 = options.getter(field, row2); 42 | } 43 | else { 44 | val1 = row1[field]; 45 | val2 = row2[field]; 46 | } 47 | if(!val1 && val1 !== 0) { 48 | val1 = ''; 49 | } 50 | if(!val2 && val2 !== 0) { 51 | val2 = ''; 52 | } 53 | return val1.toString().toLowerCase().localeCompare(val2.toString().toLowerCase()); 54 | }; 55 | }, 56 | stringFormatted: function(field){ 57 | return function(row1,row2,options,column) { 58 | var val1, val2; 59 | if (options !== undefined && {}.hasOwnProperty.call(options, 'getter')) { 60 | val1 = options.getter(field, row1); 61 | val2 = options.getter(field, row2); 62 | } 63 | else { 64 | val1 = row1[field]; 65 | val2 = row2[field]; 66 | } 67 | val1 = column.format(val1, row1, column); 68 | val2 = column.format(val2, row2, column); 69 | 70 | return val1.toString().toLowerCase().localeCompare(val2.toString().toLowerCase()); 71 | }; 72 | }, 73 | numberFormatted: function(field){ 74 | return function(row1,row2,options,column) { 75 | var val1, val2; 76 | if (options !== undefined && {}.hasOwnProperty.call(options, 'getter')) { 77 | val1 = options.getter(field, row1); 78 | val2 = options.getter(field, row2); 79 | } 80 | else { 81 | val1 = row1[field]; 82 | val2 = row2[field]; 83 | } 84 | val1 = column.format(val1, row1, column); 85 | val2 = column.format(val2, row2, column); 86 | 87 | return val1*1 - val2*1; 88 | }; 89 | }, 90 | }; 91 | }); 92 | -------------------------------------------------------------------------------- /src/templates/apMesa.tpl.html: -------------------------------------------------------------------------------- 1 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 82 | 83 | 84 |
28 | 29 | 30 | 31 | 32 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 49 |   50 | 51 | 52 |
60 | 61 | 62 | 69 | 70 | 71 | 80 | 81 |
85 |
86 |
87 | 88 | 89 | 94 | 95 | 96 | 97 | 98 | 99 |
100 |
101 |
102 |
103 | -------------------------------------------------------------------------------- /src/templates/apMesaDummyRows.tpl.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyperlitch/angularjs-table/23ff0603afd782df2e09c4b924f8fa12cbb82052/src/templates/apMesaDummyRows.tpl.html -------------------------------------------------------------------------------- /src/templates/apMesaPaginationCtrls.tpl.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | {{ options.rowsPerPageMessage }} 15 | 20 | 21 | -------------------------------------------------------------------------------- /src/templates/apMesaRows.tpl.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/templates/apMesaStatusDisplay.tpl.html: -------------------------------------------------------------------------------- 1 |
6 | 7 | 8 |
9 | 10 | 11 |
12 |
{{ options.loadingText }}
13 | 14 | 15 |
16 |
17 |
18 | 19 |
20 | 21 | 22 |
23 |
24 |
{{ options.loadingErrorText }}
25 |
An error occurred.
26 |
27 | 28 | 29 |
30 |
31 |
{{ options.noRowsText }}
32 |
33 |
-------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": false, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "strict": true, 18 | "trailing": true, 19 | "smarttabs": true, 20 | "expr": true, 21 | "globals": { 22 | "after": false, 23 | "afterEach": false, 24 | "angular": false, 25 | "before": false, 26 | "beforeEach": false, 27 | "browser": false, 28 | "describe": false, 29 | "expect": false, 30 | "inject": false, 31 | "it": false, 32 | "sinon": false, 33 | "spyOn": false, 34 | "$": false 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /test/karma-e2e.conf.js: -------------------------------------------------------------------------------- 1 | var sharedConfig = require('./karma-shared.conf'); 2 | 3 | module.exports = function(config) { 4 | var conf = sharedConfig(); 5 | 6 | conf.files = conf.files.concat([ 7 | //test files 8 | './test/e2e/**/*.js' 9 | ]); 10 | 11 | conf.proxies = { 12 | '/': 'http://localhost:9999/' 13 | }; 14 | 15 | conf.urlRoot = '/__karma__/'; 16 | 17 | conf.frameworks = ['ng-scenario']; 18 | 19 | config.set(conf); 20 | }; 21 | -------------------------------------------------------------------------------- /test/karma-midway.conf.js: -------------------------------------------------------------------------------- 1 | var sharedConfig = require('./karma-shared.conf'); 2 | 3 | module.exports = function(config) { 4 | var conf = sharedConfig(); 5 | 6 | conf.files = conf.files.concat([ 7 | //extra testing code 8 | 'node_modules/ng-midway-tester/src/ngMidwayTester.js', 9 | 10 | //mocha stuff 11 | 'test/mocha.conf.js', 12 | 13 | //test files 14 | 'test/midway/appSpec.js', 15 | 'test/midway/controllers/controllersSpec.js', 16 | 'test/midway/filters/filtersSpec.js', 17 | 'test/midway/directives/directivesSpec.js', 18 | 'test/midway/requestsSpec.js', 19 | 'test/midway/routesSpec.js', 20 | 'test/midway/**/*.js' 21 | ]); 22 | 23 | conf.proxies = { 24 | '/': 'http://localhost:9999/' 25 | }; 26 | 27 | config.set(conf); 28 | }; 29 | -------------------------------------------------------------------------------- /test/karma-shared.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | var index = fs.readFileSync(path.normalize(__dirname + '/../app/index.html'), 'utf8'); 6 | var re = /src="bower_components[^"]+"/g; 7 | var bower_scripts = index.match(re).map(function(src) { 8 | return src.replace('src="','app/').replace('"',''); 9 | }); 10 | 11 | module.exports = function() { 12 | return { 13 | basePath: '../', 14 | frameworks: ['mocha','sinon-chai', 'sinon', 'chai'], 15 | browsers: ['PhantomJS'], 16 | autoWatch: true, 17 | reporters: ['dots', 'coverage'], 18 | // plugins: ['karma-chrome-launcher','karma-mocha','karma-coverage'], 19 | 20 | // tell karma how you want the coverage results 21 | coverageReporter: { 22 | type : 'html', 23 | // where to store the report 24 | dir : 'coverage/' 25 | }, 26 | 27 | // these are default values anyway 28 | singleRun: false, 29 | colors: true, 30 | 31 | files : bower_scripts.concat([ 32 | 33 | //App-specific Code 34 | 'src/**/*.js', 35 | 'app/scripts/app.js', 36 | 37 | 38 | //Test-Specific Code 39 | 'node_modules/chai/chai.js', 40 | 'node_modules/sinon/pkg/sinon.js', 41 | 'test/lib/chai-should.js', 42 | 'test/lib/chai-expect.js' 43 | ]) 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /test/karma-unit.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var sharedConfig = require('./karma-shared.conf'); 4 | 5 | module.exports = function(config) { 6 | var conf = sharedConfig(); 7 | 8 | conf.files = conf.files.concat([ 9 | //extra testing code 10 | 'app/bower_components/angular-mocks/angular-mocks.js', 11 | 12 | //mocha stuff 13 | // 'test/mocha.conf.js', 14 | 15 | //test files 16 | './test/spec/**/*.js', 17 | 18 | // template files 19 | 'src/templates/*.tpl.html' 20 | ]); 21 | 22 | 23 | conf.preprocessors = { 24 | // which html templates to be converted to js 25 | 'src/templates/*.tpl.html': ['ng-html2js'], 26 | // files we want to appear in the coverage report 27 | // 'src/**/*.js': ['coverage'] 28 | }; 29 | 30 | conf.ngHtml2JsPreprocessor = { 31 | // strip this from the file path 32 | stripPrefix: 'app/', 33 | 34 | // setting this option will create only a single module that contains templates 35 | // from all the files, so you can load them all with module('foo') 36 | moduleName: 'apMesa.templates' 37 | }; 38 | 39 | 40 | config.set(conf); 41 | }; 42 | -------------------------------------------------------------------------------- /test/lib/chai-expect.js: -------------------------------------------------------------------------------- 1 | var expect = chai.expect; -------------------------------------------------------------------------------- /test/lib/chai-should.js: -------------------------------------------------------------------------------- 1 | var should = chai.should; -------------------------------------------------------------------------------- /test/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | End2end Test Runner 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/spec/directives/ap-mesa-cell.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive: ap-mesa-cell', function () { 4 | 5 | var element, scope, rootScope, isoScope, compile; 6 | 7 | beforeEach(function() { 8 | // define mock objects here 9 | }); 10 | 11 | // load the directive's module 12 | beforeEach(module('apMesa', function($provide, $filterProvider) { 13 | // Inject dependencies like this: 14 | $filterProvider.register('commaGroups', function() { 15 | function commaGroups(value) { 16 | if (typeof value === 'undefined') { 17 | return '-'; 18 | } 19 | var parts = value.toString().split('.'); 20 | parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); 21 | return parts.join('.'); 22 | } 23 | return commaGroups; 24 | }); 25 | 26 | })); 27 | 28 | beforeEach(inject(function ($compile, $rootScope) { 29 | // Cache these for reuse 30 | rootScope = $rootScope; 31 | compile = $compile; 32 | 33 | // Other setup, e.g. helper functions, etc. 34 | 35 | // Set up the outer scope 36 | scope = $rootScope.$new(); 37 | scope.column = { 38 | id: 'id', 39 | key: 'id' 40 | }; 41 | scope.row = { 42 | id: 'hello' 43 | }; 44 | scope.options = {}; 45 | 46 | })); 47 | 48 | afterEach(function() { 49 | // tear down here 50 | }); 51 | 52 | describe('when a template is specified', function() { 53 | 54 | beforeEach(function() { 55 | scope.column.template = '{{ row[column.key] }}'; 56 | // Define and compile the element 57 | element = angular.element('
'); 58 | element = compile(element)(scope); 59 | scope.$digest(); 60 | isoScope = element.isolateScope(); 61 | }); 62 | 63 | it('should recompile the cell with the supplied template', function() { 64 | var strong = element.find('strong'); 65 | expect(strong.length).to.equal(1); 66 | expect(strong.text()).to.equal(scope.row.id); 67 | }); 68 | 69 | }); 70 | 71 | describe('when a templateUrl is specified', function() { 72 | beforeEach(inject(function($templateCache) { 73 | scope.column.templateUrl = 'some/url.html'; 74 | $templateCache.put(scope.column.templateUrl, '{{ row[column.key] }}'); 75 | 76 | // Define and compile the element 77 | element = angular.element('
'); 78 | element = compile(element)(scope); 79 | scope.$digest(); 80 | isoScope = element.isolateScope(); 81 | })); 82 | 83 | it('should recompile the cell with the supplied templateUrl', function() { 84 | var strong = element.find('strong'); 85 | expect(strong.length).to.equal(1); 86 | expect(strong.text()).to.equal(scope.row.id); 87 | }); 88 | }); 89 | 90 | describe('when an ngFilter is specified', function() { 91 | beforeEach(inject(function($templateCache) { 92 | scope.column.ngFilter = 'commaGroups'; 93 | scope.row.id = '1000000'; 94 | $templateCache.put(scope.column.templateUrl, '{{ row[column.key] }}'); 95 | 96 | // Define and compile the element 97 | element = angular.element('
'); 98 | element = compile(element)(scope); 99 | scope.$digest(); 100 | isoScope = element.isolateScope(); 101 | })); 102 | 103 | it('should recompile the cell with the supplied filter', function() { 104 | var str = element.text(); 105 | expect(str).to.equal('1,000,000'); 106 | }); 107 | 108 | afterEach(function() { 109 | delete scope.column.ngFilter; 110 | }); 111 | }); 112 | 113 | describe('when a getter is specified on options', function() { 114 | beforeEach(function() { 115 | scope.row.data = { 116 | id: 'hello2 in row.data' 117 | }; 118 | scope.options.getter = function(key, row) { 119 | return row.data[key]; 120 | }; 121 | element = angular.element('
'); 122 | element = compile(element)(scope); 123 | scope.$digest(); 124 | }); 125 | 126 | it('should use getter', function() { 127 | expect(element.text()).to.equal(scope.row.data.id); 128 | }); 129 | }); 130 | 131 | }); 132 | -------------------------------------------------------------------------------- /test/spec/directives/ap-mesa-selector.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive: apMesaSelector', function () { 4 | 5 | var element, scope, rootScope, isoScope, compile, sandbox, selected, row, column; 6 | 7 | beforeEach(function() { 8 | sandbox = sinon.sandbox.create(); 9 | 10 | // define mock objects here 11 | }); 12 | 13 | // load the directive's module 14 | beforeEach(module('apMesa', function($provide) { 15 | // Inject dependencies like this: 16 | // $provide.value('', mockThing); 17 | 18 | })); 19 | 20 | beforeEach(inject(function ($compile, $rootScope) { 21 | // Cache these for reuse 22 | rootScope = $rootScope; 23 | compile = $compile; 24 | 25 | // Other setup, e.g. helper functions, etc. 26 | 27 | // Set up the outer scope 28 | scope = $rootScope.$new(); 29 | scope.selected = selected = []; 30 | scope.row = row = { id: 1 }; 31 | scope.column = column = { key: 'id' }; 32 | 33 | // Define and compile the element 34 | element = angular.element('
'); 35 | element = compile(element)(scope); 36 | scope.$digest(); 37 | isoScope = element.isolateScope(); 38 | })); 39 | 40 | afterEach(function() { 41 | sandbox.restore(); 42 | }); 43 | 44 | describe('the click event', function() { 45 | var e; 46 | beforeEach(function() { 47 | e = $.Event('click'); 48 | }); 49 | 50 | it('should add row[column.key] to the selected array if it is not present already', function() { 51 | $(element).trigger(e); 52 | scope.$digest(); 53 | expect(selected).to.contain(row[column.key]); 54 | }); 55 | 56 | it('when "selectObject: true" is specified in column, should add the entire row(an object instead of a number/string) to the selected array if it is not present already', function() { 57 | column.selectObject = true; 58 | $(element).trigger(e); 59 | scope.$digest(); 60 | for(var i = 0; i= 69; } 40 | if (term === 'short') { return value < 69; } 41 | return true; 42 | } 43 | feet_filter.title = filter_title = 'Type in "short" or "tall"'; 44 | feet_filter.placeholder = filter_placeholder = '"short", "tall"'; 45 | 46 | // Random data generator 47 | genRows = function(num){ 48 | var retVal = []; 49 | for (var i=0; i < num; i++) { 50 | retVal.push(genRow(i)); 51 | } 52 | return retVal; 53 | }; 54 | function genRow(id){ 55 | 56 | var fnames = ['joe','fred','frank','jim','mike','gary','aziz']; 57 | var lnames = ['sterling','smith','erickson','burke','ansari']; 58 | var seed = Math.random(); 59 | var seed2 = Math.random(); 60 | var first_name = fnames[ Math.round( seed * (fnames.length -1) ) ]; 61 | var last_name = lnames[ Math.round( seed * (lnames.length -1) ) ]; 62 | 63 | return { 64 | id: id, 65 | selected: false, 66 | first_name: first_name, 67 | last_name: last_name, 68 | age: Math.ceil(seed * 75) + 15, 69 | height: Math.round( seed2 * 36 ) + 48, 70 | weight: Math.round( seed2 * 130 ) + 90 71 | }; 72 | } 73 | 74 | scope = $rootScope.$new(); 75 | compile = $compile; 76 | timeout = $timeout; 77 | 78 | // Preload for labelTemplateUrl option 79 | $templateCache.put('example/th/template.html', 'Height!'); 80 | 81 | // Table columns 82 | scope.my_table_columns = columns = [ 83 | { 84 | id: 'selector', 85 | key: 'selected', 86 | label: '', 87 | selector: true, 88 | width: '30px', 89 | lockWidth: true 90 | }, 91 | { 92 | id: 'ID', 93 | key: 'id', 94 | sort: 'number', 95 | filter: 'number' 96 | }, 97 | { 98 | id: 'first_name', 99 | key: 'first_name', 100 | label: 'First Name', 101 | sort: 'string', 102 | filter: 'like', 103 | title: 'First names are cool' 104 | }, 105 | { 106 | id: 'last_name', 107 | key: 'last_name', 108 | label: 'Last Name', 109 | sort: 'string', 110 | filter: 'like', 111 | filter_placeholder: 'last name' 112 | }, 113 | { 114 | id: 'age', 115 | key: 'age', 116 | sort: 'number', 117 | filter: 'number', 118 | labelTemplate: 'Age' 119 | }, 120 | { 121 | id: 'height', 122 | key: 'height', 123 | label: 'Height', 124 | sort: 'number', 125 | filter: feet_filter, 126 | format: inches2feet, 127 | labelTemplateUrl: 'example/th/template.html' 128 | }, 129 | { 130 | id: 'weight', 131 | key: 'weight', 132 | label: 'Weight', 133 | sort: 'number', 134 | filter: 'number', 135 | classes: 'test-classes-option' 136 | } 137 | ]; 138 | 139 | // Table data 140 | scope.my_table_data = data = genRows(30); 141 | 142 | createElement = function() { 143 | element = angular.element(''); 144 | element = compile(element)(scope); 145 | timeout.flush(); 146 | scope.$digest(); 147 | isoScope = element.isolateScope(); 148 | // for (var k in isoScope) { 149 | // if (isoScope.hasOwnProperty(k)) { 150 | // // console.log('k: ', k, 'scope[k]', isoScope[k]); 151 | // console.log(k); 152 | // } 153 | // } 154 | }; 155 | 156 | createElement(); 157 | })); 158 | 159 | afterEach(function() { 160 | sandbox.restore(); 161 | }); 162 | 163 | it('should be okay with a delayed data set', function() { 164 | scope.my_table_data = undefined; 165 | // expect(createElement).not.to.throw(); 166 | createElement(); 167 | }); 168 | 169 | it('should create two tables', function () { 170 | expect(element.find('table').length).to.equal(2); 171 | }); 172 | 173 | it('should create an options object if one is not provided', function() { 174 | expect(isoScope.options).to.be.an('object'); 175 | }); 176 | 177 | it('should display the data passed to it', function () { 178 | var expected = data[0].first_name; 179 | var actual = element.find('table:eq(1) tbody.ap-mesa-rendered-rows tr:eq(0) td:eq(2)').text(); 180 | actual = $.trim(actual); 181 | expect(actual).to.equal(expected); 182 | }); 183 | 184 | it('should update displayed values when data has been updated', function() { 185 | scope.my_table_data = genRows(30); 186 | timeout.flush(); 187 | scope.$apply(); 188 | var expected = scope.my_table_data[0].first_name; 189 | var actual = element.find('table:eq(1) tbody.ap-mesa-rendered-rows tr:eq(0) td:eq(2)').text(); 190 | actual = $.trim(actual); 191 | expect(actual).to.equal(expected); 192 | }); 193 | 194 | it('should not throw if no columns array was found on the scope', inject(function($rootScope) { 195 | var scope2 = $rootScope.$new(); 196 | scope2.rows = []; 197 | var el2 = angular.element(''); 198 | var fn = function() { 199 | el2 = compile(el2)(scope2); 200 | scope.$digest(); 201 | }; 202 | expect(fn).not.to.throw(); 203 | })); 204 | 205 | it('should allow an options object to be passed, and should use override default options', inject(function($rootScope) { 206 | var $scope2 = $rootScope.$new(); 207 | $scope2.columns = []; 208 | $scope2.rows = []; 209 | $scope2.options = { 210 | bgSizeMultiplier: 3 211 | }; 212 | var el2 = angular.element(''); 213 | el2 = compile(el2)($scope2); 214 | $scope2.$digest(); 215 | isoScope = el2.isolateScope(); 216 | expect(isoScope.options.bgSizeMultiplier).to.equal(3); 217 | })); 218 | 219 | it('should attach a persistentState.searchTerms object to the scope', function() { 220 | expect(isoScope.persistentState.searchTerms).to.be.an('object'); 221 | }); 222 | 223 | it('should attach a sortOrder array to the scope', function() { 224 | expect(isoScope.persistentState.sortOrder).to.be.instanceof(Array); 225 | }); 226 | 227 | it('options.getter should be a function', function() { 228 | // isoScope.options.getter = function() { 229 | // return 'valueFromGetter'; 230 | // }; 231 | if (isoScope.options !== undefined && {}.hasOwnProperty.call(isoScope.options, 'getter')) { 232 | expect(isoScope.options.getter).to.be.an('function'); 233 | } 234 | }); 235 | 236 | it('should set a default trackBy to "id"', function() { 237 | expect(isoScope.options.trackBy).to.equal('id'); 238 | }); 239 | 240 | describe('column header', function() { 241 | 242 | it('should have a .column-resizer element if lockWidth is not set', function() { 243 | expect(element.find('table:eq(0) th:eq(1) .column-resizer').length).to.equal(1); 244 | }); 245 | 246 | it('should not have a .column-resizer element if lockWidth is set to true', function() { 247 | expect(element.find('table:eq(0) th:eq(0) .column-resizer').length).to.equal(0); 248 | }); 249 | 250 | it('should set the style to column.width if supplied in column definition', function() { 251 | expect(element.find('table:eq(0) th:eq(0)').css('width')).to.equal(columns[0].width); 252 | }); 253 | 254 | it('should display column.id if column.label is not specified', function() { 255 | var actual = $.trim(element.find('table:eq(0) th:eq(1) .column-text').text()); 256 | expect(actual).to.equal(columns[1].id); 257 | }); 258 | 259 | it('should display column.label if it is present', function() { 260 | var actual = $.trim(element.find('table:eq(0) th:eq(2) .column-text').text()); 261 | expect(actual).to.equal(columns[2].label); 262 | }); 263 | 264 | it('should display column.label if it is present, even if it is a falsey value', function() { 265 | var actual = $.trim(element.find('table:eq(0) th:eq(0) .column-text').text()); 266 | expect(actual).to.equal(columns[0].label); 267 | }); 268 | 269 | it('should attach a title (tooltip) to s where title was specified in column definition', function() { 270 | var actual = element.find('table:eq(0) th:eq(2)').attr('title'); 271 | var expected = columns[2].title; 272 | expect(actual).to.equal(expected); 273 | }); 274 | 275 | it('should have a col-header-{id} class on each ', function() { 276 | expect(element.find('table:eq(0) th:eq(2)').hasClass('table-header-first_name')).to.equal(true); 277 | }); 278 | 279 | it('should have a "sortable-column" class on each whose column has a sort', function() { 280 | scope.my_table_columns.forEach(function(col, i) { 281 | if (col.sort) { 282 | expect(element.find('table:eq(0) th:eq(' + i + ')').hasClass('sortable-column')).to.equal(true); 283 | } 284 | }); 285 | }); 286 | 287 | it('should use the labelTemplate option', function() { 288 | expect(element.find('table:eq(0) th:eq(4) span.test-labelTemplate').length).to.equal(1); 289 | }); 290 | 291 | it('should use the labelTemplateUrl option', function() { 292 | expect(element.find('table:eq(0) th:eq(5) span.test-labelTemplateUrl').length).to.equal(1); 293 | }); 294 | 295 | it('should add any classes specified in the classes option to the header', function() { 296 | expect(element.find('table:eq(0) th:eq(6)').hasClass('test-classes-option')).to.equal(true); 297 | }); 298 | 299 | 300 | 301 | }); 302 | 303 | describe('column filter', function() { 304 | 305 | it('should have a placeholder if specified as a property on the filter function', function() { 306 | var actual = element.find('table:eq(0) tr:eq(1) td:eq(5) input').attr('placeholder'); 307 | var expected = filter_placeholder; 308 | expect(actual).to.equal(expected); 309 | }); 310 | 311 | it('should have a title if specified as a property on the filter function', function() { 312 | var actual = element.find('table:eq(0) tr:eq(1) td:eq(5) input').attr('title'); 313 | var expected = filter_title; 314 | expect(actual).to.equal(expected); 315 | }); 316 | 317 | }); 318 | 319 | 320 | }); 321 | -------------------------------------------------------------------------------- /test/spec/filters/table-row-filter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Filter: apMesaRowFilter', function() { 4 | 5 | var columns, rows, persistentState, filter, fakeSearchFn1, fakeSearchFn2, sandbox, mockLog; 6 | 7 | beforeEach(function() { 8 | sandbox = sinon.sandbox.create(); 9 | }); 10 | 11 | // load the filter's module 12 | beforeEach(module('apMesa', function($provide) { 13 | mockLog = { warn: sandbox.spy() }; 14 | $provide.value('$log', mockLog); 15 | })); 16 | 17 | beforeEach(inject(function($filter){ 18 | filter = $filter('apMesaRowFilter'); 19 | 20 | fakeSearchFn1 = function(term, value) { 21 | return value === term; 22 | }; 23 | 24 | fakeSearchFn2 = function(term, value) { 25 | return value === 20; 26 | }; 27 | 28 | columns = [ 29 | { id: 'fname', key: 'fname', filter: fakeSearchFn1, format: sandbox.stub().returns('FORMATTED') }, 30 | { id: 'col2', key: 'col2', filter: fakeSearchFn2 }, 31 | { id: 'col3', key: 'col3', filter: 'like' }, 32 | { id: 'col4', key: 'col4', filter: 'invalidFilterName' } 33 | ]; 34 | rows = [ 35 | { fname: 'phu', col2: 10, col3: 'foo' }, 36 | { fname: 'amol', col2: 20, col3: 'foobar' }, 37 | { fname: 'henry', col2: 30, col3: 'bar' } 38 | ]; 39 | persistentState = { 40 | searchTerms: {}, 41 | sortOrder: [] 42 | }; 43 | })); 44 | 45 | afterEach(function() { 46 | sandbox.restore(); 47 | }); 48 | 49 | it('should return all rows if no search terms are set', function() { 50 | expect(filter(rows, columns, persistentState, {})).to.equal(rows); 51 | }); 52 | 53 | it('should ignore search terms that are empty strings or only whitespace', function() { 54 | ['', ' ', ' '].forEach(function(val) { 55 | persistentState.searchTerms.fname = val; 56 | expect(filter(rows, columns, persistentState, {})).to.equal(rows); 57 | }); 58 | }); 59 | 60 | it('should turn on a search function when a value is present in searchTerms', function() { 61 | persistentState.searchTerms.fname = 'phu'; 62 | var results = filter(rows, columns, persistentState, {}); 63 | expect( results.length ).to.equal(1); 64 | expect( results[0] ).to.equal(rows[0]); 65 | 66 | persistentState.searchTerms.fname = ''; 67 | persistentState.searchTerms.col2 = 'some search'; 68 | var results2 = filter(rows, columns, persistentState, {}); 69 | expect( results2.length ).to.equal(1); 70 | expect( results2[0] ).to.equal(rows[1]); 71 | }); 72 | 73 | it('should ignore invalid predefined filter names and call $log.warn', function() { 74 | persistentState.searchTerms.col4 = 'some search'; 75 | var results = filter(rows, columns, persistentState, {}); 76 | expect(results).to.equal(rows); 77 | expect(mockLog.warn).to.have.been.calledOnce; 78 | }); 79 | 80 | it('should replace string references to built-in filter functions with actual functions', function() { 81 | persistentState.searchTerms.col3 = 'foo'; 82 | filter(rows, columns, persistentState, {}); 83 | expect(columns[2].filter).to.be.a('function'); 84 | }); 85 | 86 | it('should call the filter function with term, value, computedValue, and the row in that order', function() { 87 | // spy on filter fn 88 | sandbox.spy(columns[0], 'filter'); 89 | persistentState.searchTerms.fname = 'test search'; 90 | filter(rows, columns, persistentState, {}); 91 | expect(columns[0].filter).to.have.been.calledWith('test search','phu','FORMATTED',rows[0]); 92 | expect(columns[0].filter).to.have.been.calledWith('test search','amol','FORMATTED',rows[1]); 93 | expect(columns[0].filter).to.have.been.calledWith('test search','henry','FORMATTED',rows[2]); 94 | }); 95 | 96 | }); 97 | -------------------------------------------------------------------------------- /test/spec/filters/table-row-sorter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Filter: tableRowSorter', function() { 4 | 5 | var sandbox, sorter, columns, rows, numSort, numSort2, stringSort, sortOrder, transientState; 6 | 7 | beforeEach(module('apMesa')); 8 | 9 | beforeEach(inject(function(apMesaRowSorterFilter) { 10 | sandbox = sinon.sandbox.create(); 11 | 12 | sorter = apMesaRowSorterFilter; 13 | 14 | stringSort = sandbox.spy(function(a,b) { 15 | return a.key1 < b.key1 ? -1 : a.key1 > b.key1 ? 1 : 0 ; 16 | }); 17 | numSort = sandbox.spy(function(a,b) { 18 | return a.key2 - b.key2; 19 | }); 20 | numSort2 = sandbox.spy(function(a,b) { 21 | return a.key3 - b.key3; 22 | }); 23 | 24 | columns = [ 25 | { id: 'key1', key: 'key1', sort: stringSort }, 26 | { id: 'key2', key: 'key2', sort: numSort }, 27 | { id: 'key3', key: 'key3', sort: numSort } 28 | ]; 29 | rows = [ 30 | { index: 0, key1:'c', key2:2, key3: 4 }, 31 | { index: 1, key1:'b', key2:1, key3: 4 }, 32 | { index: 2, key1:'a', key2:3, key3: 2 }, 33 | { index: 3, key1:'b', key2:3, key3: 3 } 34 | ]; 35 | transientState = { 36 | columnLookup: {} 37 | }; 38 | columns.forEach(function(column) { 39 | transientState.columnLookup[column.id] = column; 40 | }); 41 | })); 42 | 43 | afterEach(function() { 44 | sandbox.restore(); 45 | }); 46 | 47 | it('should be a function', function() { 48 | expect(sorter).to.be.a('function'); 49 | }); 50 | 51 | it('should return all rows if no sorting is active', function() { 52 | expect(sorter(rows,columns,[],{}, transientState)).to.equal(rows); 53 | }); 54 | 55 | it('should sort ascending by a column whose "sorting" field is "+"', function() { 56 | 57 | sortOrder = [{id: 'key1', dir: '+'}]; 58 | 59 | var result = sorter(rows, columns, sortOrder, {}, transientState); 60 | var idxs = result.map(function(r){ return r.index; }); 61 | expect(idxs).to.eql([2,1,3,0]); 62 | 63 | }); 64 | 65 | it('should sort descending by a column whose "sorting" field is "-"', function() { 66 | 67 | sortOrder = [{id:'key1', dir: '-'}]; 68 | 69 | var result = sorter(rows,columns,sortOrder, {}, transientState); 70 | var idxs = result.map(function(r){ return r.index; }); 71 | expect(idxs).to.eql([0,1,3,2]); 72 | 73 | }); 74 | 75 | it('should ignore sort columns in sortOrder that do not exist', function() { 76 | sortOrder = [{id:'not_a_column', dir: '+'},{id:'key1', dir: '-'}]; 77 | 78 | var result = sorter(rows,columns,sortOrder, {}, transientState); 79 | var idxs = result.map(function(r){ return r.index; }); 80 | expect(idxs).to.eql([0,1,3,2]); 81 | }); 82 | 83 | }); 84 | -------------------------------------------------------------------------------- /test/spec/services/table-format-functions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | describe('Service: apMesaFormatFunctions', function() { 3 | 4 | beforeEach(module('apMesa')); 5 | 6 | var formats; 7 | 8 | beforeEach(inject(['apMesaFormatFunctions', function(tableFormats) { 9 | formats = tableFormats; 10 | }])); 11 | 12 | it('should return an object containing functions', function() { 13 | expect(formats).to.be.an('object'); 14 | angular.forEach(formats, function(fn) { 15 | expect(fn).to.be.a('function'); 16 | }); 17 | }); 18 | 19 | }); 20 | -------------------------------------------------------------------------------- /test/spec/services/table-sort-functions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Service: tableSortFunctions', function() { 4 | 5 | var sandbox, sorts; 6 | 7 | beforeEach(function() { 8 | sandbox = sinon.sandbox.create(); 9 | }); 10 | 11 | beforeEach(module('apMesa')); 12 | 13 | beforeEach(inject(['apMesaSortFunctions', function(s) { 14 | sorts = s; 15 | }])); 16 | 17 | afterEach(function() { 18 | sandbox.restore(); 19 | }); 20 | 21 | it('should be an object', function() { 22 | expect(sorts).to.be.an('object'); 23 | }); 24 | 25 | describe('number sorting', function() { 26 | 27 | var factory, sorter; 28 | 29 | beforeEach(function() { 30 | factory = sorts.number; 31 | sorter = factory('fieldname'); 32 | }); 33 | 34 | it('should be a function', function() { 35 | expect(factory).to.be.a('function'); 36 | }); 37 | 38 | it('should return a function', function() { 39 | expect(sorter).to.be.a('function'); 40 | }); 41 | 42 | it('should return less than 0 if the value in first object is less than that of second', function() { 43 | expect( sorter( { fieldname: 1 }, { fieldname: 3 } ) ).to.be.below(0); 44 | }); 45 | 46 | it('should return greater than 0 if the value in first object is more than that of second', function() { 47 | expect( sorter( { fieldname: 3 }, { fieldname: 1 } ) ).to.be.above(0); 48 | }); 49 | 50 | it('should return 0 if values on both objects are the same', function() { 51 | expect( sorter( { fieldname: 5 }, { fieldname: 5 } ) ).to.equal(0); 52 | }); 53 | 54 | it('should try and convert strings', function() { 55 | expect( sorter( { fieldname: '1' }, { fieldname: '3' } ) ).to.be.below(0); 56 | expect( sorter( { fieldname: '3' }, { fieldname: '1' } ) ).to.be.above(0); 57 | expect( sorter( { fieldname: '5' }, { fieldname: '5' } ) ).to.equal(0); 58 | }); 59 | 60 | }); 61 | 62 | describe('number formatted sorting', function() { 63 | 64 | var factory, sorter, pseudoColumn, lookup; 65 | 66 | beforeEach(function() { 67 | factory = sorts.numberFormatted; 68 | sorter = factory('fieldname'); 69 | lookup = { 70 | 1: 10, 71 | 2: 9, 72 | 3: 8, 73 | 5: 11, 74 | 7: 11, 75 | 9: '1', 76 | 10: '2', 77 | 11: '3' 78 | }; 79 | pseudoColumn = { 80 | format: function(raw) { 81 | return lookup[raw]; 82 | } 83 | }; 84 | }); 85 | 86 | it('should be a function', function() { 87 | expect(factory).to.be.a('function'); 88 | }); 89 | 90 | it('should return a function', function() { 91 | expect(sorter).to.be.a('function'); 92 | }); 93 | 94 | it('should return less than 0 if the value in first object is less than that of second', function() { 95 | expect( sorter( { fieldname: 3 }, { fieldname: 1 }, {}, pseudoColumn ) ).to.be.below(0); 96 | expect( sorter( { fieldname: 3 }, { fieldname: 2 }, {}, pseudoColumn ) ).to.be.below(0); 97 | }); 98 | 99 | it('should return greater than 0 if the value in first object is more than that of second', function() { 100 | expect( sorter( { fieldname: 1 }, { fieldname: 2 }, {}, pseudoColumn ) ).to.be.above(0); 101 | expect( sorter( { fieldname: 1 }, { fieldname: 3 }, {}, pseudoColumn ) ).to.be.above(0); 102 | }); 103 | 104 | it('should return 0 if values on both objects are the same', function() { 105 | expect( sorter( { fieldname: 5 }, { fieldname: 7 }, {}, pseudoColumn ) ).to.equal(0); 106 | }); 107 | 108 | it('should try and convert strings', function() { 109 | expect( sorter( { fieldname: '9' }, { fieldname: '10' }, {}, pseudoColumn ) ).to.be.below(0); 110 | expect( sorter( { fieldname: '11' }, { fieldname: '10' }, {}, pseudoColumn ) ).to.be.above(0); 111 | expect( sorter( { fieldname: '11' }, { fieldname: '11' }, {}, pseudoColumn ) ).to.equal(0); 112 | }); 113 | 114 | }); 115 | 116 | describe('string sorting', function() { 117 | 118 | var factory, sorter; 119 | 120 | beforeEach(function() { 121 | factory = sorts.string; 122 | sorter = factory('fieldname'); 123 | }); 124 | 125 | it('should be a function', function() { 126 | expect(factory).to.be.a('function'); 127 | }); 128 | 129 | it('should return a function', function() { 130 | expect(sorter).to.be.a('function'); 131 | }); 132 | 133 | it('should return less than 0 if the first value is alphabetically before the second value', function() { 134 | expect(sorter({fieldname: 'a'},{fieldname: 'b'})).to.be.below(0); 135 | }); 136 | 137 | it('should return greater than 0 if the first value is alphabetically after the second value', function() { 138 | expect(sorter({fieldname: 'c'},{fieldname: 'b'})).to.be.above(0); 139 | }); 140 | 141 | it('should return 0 if both are the same', function() { 142 | expect(sorter({fieldname: 'c'},{fieldname: 'c'})).to.equal(0); 143 | }); 144 | 145 | it('should ignore case when comparing', function() { 146 | expect(sorter({fieldname: 'c'},{fieldname: 'C'})).to.equal(0); 147 | }); 148 | 149 | }); 150 | 151 | describe('string formatted sorting', function() { 152 | 153 | var factory, sorter, pseudoColumn, lookup; 154 | 155 | beforeEach(function() { 156 | factory = sorts.stringFormatted; 157 | sorter = factory('fieldname'); 158 | lookup = { 159 | a: 'z', 160 | b: 'x', 161 | c: 'y', 162 | d: 'y' 163 | }; 164 | pseudoColumn = { 165 | format: function(raw) { 166 | return lookup[raw]; 167 | } 168 | }; 169 | }); 170 | 171 | it('should be a function', function() { 172 | expect(factory).to.be.a('function'); 173 | }); 174 | 175 | it('should return a function', function() { 176 | expect(sorter).to.be.a('function'); 177 | }); 178 | 179 | it('should return less than 0 if the first formatted value is alphabetically before the second formatted value', function() { 180 | expect(sorter({fieldname: 'b'},{fieldname: 'a'}, {}, pseudoColumn)).to.be.below(0); 181 | }); 182 | 183 | it('should return greater than 0 if the first formatted value is alphabetically after the second formatted value', function() { 184 | expect(sorter({fieldname: 'a'},{fieldname: 'b'}, {}, pseudoColumn)).to.be.above(0); 185 | }); 186 | 187 | it('should return 0 if both are the same', function() { 188 | expect(sorter({fieldname: 'c'},{fieldname: 'c'}, {}, pseudoColumn)).to.equal(0); 189 | }); 190 | 191 | it('should ignore case when comparing', function() { 192 | expect(sorter({fieldname: 'c'},{fieldname: 'd'}, {}, pseudoColumn)).to.equal(0); 193 | }); 194 | 195 | }); 196 | 197 | }); 198 | -------------------------------------------------------------------------------- /update-gh-pages.sh: -------------------------------------------------------------------------------- 1 | # Updates the github pages 2 | 3 | 4 | # Reset the branch 5 | git checkout master 6 | git branch -D gh-pages 7 | git checkout -b gh-pages 8 | 9 | # Link demo files and create templates file 10 | for f in $(ls app); do ln -sf app/$f; done 11 | rm -f app/scripts/templates.js 12 | grunt html2js:dist 13 | mv dist/templates.js app/scripts/templates.js 14 | 15 | # Install bower stuff 16 | bower install -f 17 | ln -sf app/bower_components 18 | 19 | # Stage and commit 20 | git add app/bower_components -f 21 | git add -A 22 | COMMIT_ID=$(git rev-parse HEAD) 23 | git commit -m "gh-pages update for commit: ${COMMIT_ID}" 24 | 25 | # Push to remote 26 | git push --set-upstream origin gh-pages -f 27 | cp -r app/bower_components app/tmp_bower_components 28 | git checkout master 29 | rm -rf app/bower_components 30 | mv app/tmp_bower_components app/bower_components 31 | git clean -f 32 | --------------------------------------------------------------------------------