├── .editorconfig ├── .gitignore ├── .jscsrc ├── .jshintignore ├── .jshintrc ├── .npmignore ├── .release.json ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── demo ├── build │ └── app.js ├── css │ └── main.css ├── images │ └── angular@2x.png ├── index.html ├── js │ ├── bootstrap.js │ ├── controllers │ │ └── main-ctrl.js │ ├── demos.js │ ├── directives │ │ ├── code-preview.html │ │ └── code-preview.js │ ├── filter │ │ └── uppercase-first.js │ ├── modules.js │ ├── routing.js │ └── services │ │ ├── dataFactory.js │ │ └── demoMap.js └── templates │ ├── menu.html │ └── preview.html ├── dist ├── ngHandsontable.js └── ngHandsontable.min.js ├── index.html ├── karma.conf.js ├── package.json ├── src ├── directives │ ├── hotAutocomplete.js │ ├── hotColumn.js │ └── hotTable.js ├── ie-shim.js ├── ngHandsontable.js └── services │ ├── autoCompleteFactory.js │ ├── hotRegisterer.js │ └── settingFactory.js └── test ├── directives ├── hotAutocomplete.spec.js ├── hotColumn.spec.js ├── hotColumn │ └── watchingOptions.spec.js ├── hotTable.spec.js └── hotTable │ ├── watchingHooks.spec.js │ └── watchingOptions.spec.js ├── phantom-polyfill.js └── services ├── autoCompleteFactory.spec.js ├── hotRegisterer.spec.js └── settingFactory.spec.js /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | .idea 4 | /node_modules 5 | /bower_components 6 | dev.html 7 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "disallowSpacesInNamedFunctionExpression": { 4 | "beforeOpeningRoundBrace": true 5 | }, 6 | "disallowSpacesInFunctionExpression": { 7 | "beforeOpeningRoundBrace": true 8 | }, 9 | "disallowSpacesInAnonymousFunctionExpression": { 10 | "beforeOpeningRoundBrace": true 11 | }, 12 | "disallowSpacesInFunctionDeclaration": { 13 | "beforeOpeningRoundBrace": true 14 | }, 15 | "disallowEmptyBlocks": true, 16 | "disallowSpacesInCallExpression": true, 17 | "disallowSpacesInsideArrayBrackets": true, 18 | "disallowSpacesInsideParentheses": true, 19 | "disallowQuotedKeysInObjects": true, 20 | "disallowSpaceAfterObjectKeys": true, 21 | "disallowSpaceAfterPrefixUnaryOperators": true, 22 | "disallowSpaceBeforePostfixUnaryOperators": true, 23 | "disallowSpaceBeforeBinaryOperators": [ 24 | "," 25 | ], 26 | "disallowMixedSpacesAndTabs": true, 27 | "disallowTrailingWhitespace": true, 28 | "requireTrailingComma": false, 29 | "disallowYodaConditions": true, 30 | "disallowKeywords": [ "with" ], 31 | "disallowKeywordsOnNewLine": ["else"], 32 | "disallowMultipleLineBreaks": true, 33 | "disallowMultipleLineStrings": true, 34 | "disallowMultipleVarDecl": false, 35 | "disallowSpaceBeforeComma": true, 36 | "disallowSpaceBeforeSemicolon": true, 37 | "requireSpaceBeforeBlockStatements": true, 38 | "requireParenthesesAroundIIFE": true, 39 | "requireSpacesInConditionalExpression": true, 40 | "requireBlocksOnNewline": 1, 41 | "requireCommaBeforeLineBreak": true, 42 | "requireSpaceBeforeBinaryOperators": true, 43 | "requireSpaceAfterBinaryOperators": true, 44 | "requireCamelCaseOrUpperCaseIdentifiers": true, 45 | "requireLineFeedAtFileEnd": true, 46 | "requireCapitalizedConstructors": true, 47 | "requireDotNotation": true, 48 | "requireSpacesInForStatement": true, 49 | "requireSpaceBetweenArguments": true, 50 | "requireCurlyBraces": [ 51 | "if", 52 | "else", 53 | "for", 54 | "while", 55 | "do", 56 | "switch" 57 | ], 58 | "requireSpaceAfterKeywords": [ 59 | "if", 60 | "else", 61 | "for", 62 | "while", 63 | "do", 64 | "switch", 65 | "case", 66 | "return", 67 | "try", 68 | "catch", 69 | "typeof" 70 | ], 71 | "requirePaddingNewLinesBeforeLineComments": false, 72 | "requirePaddingNewLinesAfterBlocks": false, 73 | "requireSemicolons": true, 74 | "validateLineBreaks": "LF", 75 | "validateQuoteMarks": "'", 76 | "validateIndentation": 2, 77 | "disallowNotOperatorsInConditionals": true 78 | } 79 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | Gruntfile.js 3 | dist/* 4 | lib/* 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "boss": false, 3 | "browser": true, 4 | "camelcase": true, 5 | "curly": true, 6 | "debug": false, 7 | "devel": false, 8 | "eqeqeq": false, // Should be true 9 | "eqnull": true, // Should be false 10 | "evil": false, 11 | "forin": true, 12 | "freeze": true, 13 | "funcscope": false, 14 | "immed": true, 15 | "indent": 2, 16 | "latedef": false, 17 | "laxbreak": false, 18 | "laxcomma": true, // Should be false 19 | "loopfunc": false, 20 | "maxdepth": 5, 21 | "maxlen": 190, 22 | "maxparams": 9, 23 | "multistr": false, 24 | "newcap": true, 25 | "nonbsp": true, 26 | "nonew": true, 27 | "notypeof": false, 28 | "predef": [], 29 | "proto": false, 30 | "shadow": true, // Should be false 31 | "sub": true, // Should be false 32 | "supernew": false, 33 | "undef": false, 34 | "unused": false 35 | } 36 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Editor conf 2 | .idea 3 | dump.rdb 4 | 5 | # Logs 6 | logs 7 | *.log 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | coverage 15 | node_modules 16 | bower_components 17 | 18 | # Common 19 | tmp 20 | .DS_Store 21 | */.DS_Store 22 | */*/.DS_Store 23 | lib-cov 24 | dev.html 25 | -------------------------------------------------------------------------------- /.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "package_file_location": "./package.json", 3 | "no_confirm": false, 4 | "skip_git_pull": false, 5 | "skip_git_push": false, 6 | "release_message": true, 7 | "remote": "origin", 8 | "pre_commit_commands": [ 9 | "npm run build", 10 | "npm run test" 11 | ], 12 | "post_commit_commands": [], 13 | "post_complete_commands": [ 14 | "npm publish" 15 | ], 16 | "files_to_commit": [ 17 | "./dist/**/*", 18 | "./demo/**/*" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | sudo: false 4 | 5 | node_js: 6 | - '0.12' 7 | 8 | before_script: 9 | - export DISPLAY=:99.0 10 | - sh -e /etc/init.d/xvfb start 11 | - npm install -g grunt-cli 12 | 13 | notifications: 14 | email: false 15 | slack: 16 | secure: RVZTvjJhyjbZ+z7D7enUNWMBuHW6It9+33ArMcO1RbfuwzlA5aeQl+PnpDNNYITnEig1xOoSs4blBj7rLSJlC9xigLS8qzsE8Q+SLY6dvXRZ6l0dmIaduwijO0f5sPsd2neoDN3fFihlWhQKBuU/+3R9zRTfma3PS0nU1x5yHnM= 17 | on_failure: change 18 | on_success: change 19 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is used to build ngHandsontable from `src/*` 3 | * 4 | * Installation: 5 | * 1. Install Grunt CLI (`npm install -g grunt-cli`) 6 | * 1. Install Grunt 0.4.0 and other dependencies (`npm install`) 7 | * 8 | * Build: 9 | * Execute `grunt` from root directory of this directory (where Gruntfile.js is) 10 | * To execute automatically after each change, execute `grunt --force default watch` 11 | * 12 | * Result: 13 | * building ngHandsontable will create files: 14 | * - dist/ngHandsontable.js 15 | * - dist/ngHandsontable.min.js 16 | * 17 | * See http://gruntjs.com/getting-started for more information about Grunt 18 | */ 19 | module.exports = function (grunt) { 20 | var myBanner = '/**\n' + 21 | ' * <%= pkg.name %> <%= pkg.version %>\n' + 22 | ' * \n' + 23 | ' * Copyright 2012-2015 Marcin Warpechowski\n' + 24 | ' * Copyright 2015 Handsoncode sp. z o.o. \n' + 25 | ' * Licensed under the MIT license.\n' + 26 | ' * https://github.com/handsontable/ngHandsontable\n' + 27 | ' * Date: <%= (new Date()).toString() %>\n' + 28 | '*/\n\n'; 29 | 30 | grunt.initConfig({ 31 | pkg: grunt.file.readJSON('package.json'), 32 | 33 | concat: { 34 | options: { 35 | banner: myBanner 36 | }, 37 | new: { 38 | src: [ 39 | 'src/ie-shim.js', 40 | 'src/ngHandsontable.js', 41 | 'src/services/*.js', 42 | 'src/directives/*.js' 43 | ], 44 | dest: 'dist/ngHandsontable.js' 45 | } 46 | }, 47 | 48 | uglify: { 49 | options: { 50 | banner: myBanner 51 | }, 52 | "dist/ngHandsontable.min.js": ["dist/ngHandsontable.js"] 53 | }, 54 | 55 | jshint: { 56 | options: { 57 | jshintrc: true 58 | }, 59 | files:['src/**/*.js', 'src/*.js'] 60 | }, 61 | 62 | jscs: { 63 | main: { 64 | files: { 65 | src: ['src/**/*.js', 'src/*.js'] 66 | } 67 | }, 68 | options: { 69 | config: '.jscsrc', 70 | esnext: true, 71 | verbose: true 72 | } 73 | }, 74 | 75 | watch: { 76 | files: ['src/**/*.js'], 77 | tasks: ['concat', 'uglify'] 78 | }, 79 | 80 | browserify: { 81 | demoApp: { 82 | src: [ 83 | './demo/js/**/*.js' 84 | ], 85 | dest: './demo/build/app.js', 86 | options: { 87 | alias: [ 88 | './demo/js/demos.js:demos', 89 | './demo/js/modules.js:modules', 90 | './demo/js/routing.js:routing' 91 | ], 92 | transform: [ 93 | ['browserify-replace', { 94 | replace: [ 95 | { from: /@@version/, to: '<%= pkg.version %>' } 96 | ] 97 | } 98 | ] 99 | ] 100 | } 101 | }, 102 | demoAppWatch: { 103 | src: [ 104 | './demo/js/**/*.js' 105 | ], 106 | dest: './demo/build/app.js', 107 | options: { 108 | alias: [ 109 | './demo/js/demos.js:demos', 110 | './demo/js/modules.js:modules', 111 | './demo/js/routing.js:routing' 112 | ], 113 | watch: true, 114 | keepAlive: true 115 | } 116 | } 117 | } 118 | }); 119 | 120 | grunt.registerTask('build', ['jscs', 'jshint', 'concat', 'uglify']); 121 | grunt.registerTask('default', ['build']); 122 | grunt.registerTask('watch-demo-app', ['browserify:demoAppWatch']); 123 | grunt.registerTask('build-demo-app', ['browserify:demoApp']); 124 | grunt.registerTask('build-release', ['build', 'build-demo-app']); 125 | 126 | grunt.loadNpmTasks('grunt-browserify'); 127 | grunt.loadNpmTasks('grunt-contrib-concat'); 128 | grunt.loadNpmTasks('grunt-contrib-jshint'); 129 | grunt.loadNpmTasks('grunt-contrib-uglify'); 130 | grunt.loadNpmTasks('grunt-contrib-watch'); 131 | grunt.loadNpmTasks('grunt-jscs'); 132 | }; 133 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2012 Marcin Warpechowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ngHandsontable - the AngularJS directive for [Handsontable](https://github.com/handsontable/handsontable) [![Build Status](https://travis-ci.org/handsontable/ngHandsontable.png?branch=master)](https://travis-ci.org/handsontable/ngHandsontable) 2 | 3 | Enables creation of data grid applications in AngularJS. 4 | 5 | ## Demo 6 | 7 | See the demo at http://handsontable.github.io/ngHandsontable. 8 | 9 | ## Usage 10 | 11 | Include the library files: 12 | 13 | ```html 14 | 15 | 16 | 17 | 18 | ``` 19 | 20 | Include component to your app: 21 | 22 | ```js 23 | angular.module('my-app', ['ngHandsontable']); 24 | ``` 25 | 26 | Template: 27 | 28 | ```html 29 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ``` 46 | 47 | Controller: 48 | 49 | ```javascript 50 | $scope.db.items = [ 51 | { 52 | "id": 1, 53 | "name": { 54 | "first": "John", 55 | "last": "Schmidt" 56 | }, 57 | "address": "45024 France", 58 | "price": 760.41, 59 | "isActive": "Yes", 60 | "product": { 61 | "description": "Fried Potatoes", 62 | "options": [ 63 | { 64 | "description": "Fried Potatoes", 65 | "image": "//a248.e.akamai.net/assets.github.com/images/icons/emoji/fries.png" 66 | }, 67 | { 68 | "description": "Fried Onions", 69 | "image": "//a248.e.akamai.net/assets.github.com/images/icons/emoji/fries.png" 70 | } 71 | ] 72 | } 73 | }, 74 | //more items go here 75 | ]; 76 | ``` 77 | 78 | ## Directives and attributes specification 79 | 80 | Main directive for creating table is ``. For defining column options you can use settings object with 81 | columns property. If you want to describe column behavior in declarative way you can add `` directive 82 | as a children of `` element and add all neccessary attributes to describe column options. 83 | 84 | All **Handsontable** options listed [here](http://docs.handsontable.com/Options.html) should be supported. 85 | Options in camelCase mode should be passed to the directive in hyphenate mode e.q `autoWrapCol: true` -> ``. 86 | 87 | It's recommended to put all your settings in one big object (`settings="ctrl.settings"`). 88 | Settings attribute unlike any other attributes is not watched so using this can be helpful to achieve higher performance. 89 | 90 | ## License 91 | 92 | The MIT License (see the [LICENSE](https://github.com/handsontable/ngHandsontable/blob/master/LICENSE) file for the full text) 93 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngHandsontable", 3 | "dependencies": { 4 | "angular": "~1.5.0", 5 | "handsontable": "~0.28.4" 6 | }, 7 | "homepage": "https://github.com/handsontable/ngHandsontable", 8 | "authors": [ 9 | "Handsoncode", 10 | "Handsoncode " 11 | ], 12 | "description": "AngularJS directive for Handsontable", 13 | "main": "dist/ngHandsontable.js", 14 | "keywords": [ 15 | "angular", 16 | "angularjs", 17 | "handsontable", 18 | "datagrid", 19 | "grid", 20 | "table" 21 | ], 22 | "license": "MIT", 23 | "ignore": [ 24 | "**/.*", 25 | "bower_components", 26 | "demo", 27 | "node_modules", 28 | "src", 29 | "test", 30 | "tests" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /demo/build/app.js: -------------------------------------------------------------------------------- 1 | require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o li { 343 | display: block; 344 | position: relative; 345 | } 346 | 347 | .demo-nav ul > li > a { 348 | font-size: 19px; 349 | font-weight: bolder; 350 | padding-left: 15px; 351 | } 352 | 353 | .demo-nav ul > li > ul { 354 | padding: 0; 355 | margin: 0 0 10px 0; 356 | } 357 | 358 | .demo-nav ul > li > ul > li > a { 359 | padding-top: 1px; 360 | padding-bottom: 1px; 361 | padding-left: 30px; 362 | font-size: 13px; 363 | color: #777; 364 | font-weight: normal; 365 | } 366 | 367 | .demo-nav li.active a { 368 | font-weight: bold; 369 | color: #000; 370 | } 371 | 372 | .demo-content { 373 | width: 82%; 374 | float: left; 375 | position: relative; 376 | } 377 | 378 | .home #wrapper { 379 | max-width: 1000px; 380 | } 381 | 382 | .logo-desc { 383 | margin-top: 0; 384 | } 385 | 386 | .warning { 387 | text-align: center; 388 | color: #AA0000; 389 | } 390 | 391 | .sub { 392 | font-weight: bold; 393 | } 394 | 395 | .hidden { 396 | display: none !important; 397 | visibility: hidden; 398 | } 399 | 400 | .clearfix:before, .clearfix:after { 401 | content: ""; 402 | display: table; 403 | } 404 | 405 | .clearfix:after { 406 | clear: both; 407 | } 408 | 409 | .clearfix { 410 | *zoom: 1; 411 | } 412 | 413 | code.hljs { 414 | background-color: transparent; 415 | } 416 | 417 | .fork-me img { 418 | border: 0; 419 | height: 149px; 420 | position: absolute; 421 | right: 0; 422 | top: 0; 423 | width: 149px; 424 | } 425 | 426 | .angular { 427 | border: 0; 428 | height: 49px; 429 | position: absolute; 430 | top: 20px; 431 | width: 49px; 432 | /*left: 705px;*/ 433 | right: 24%; 434 | } 435 | 436 | .example { 437 | padding: 5px 30px 0 30px; 438 | display: flex; 439 | flex-flow: row wrap; 440 | justify-content: space-around; 441 | } 442 | 443 | .handsontable { 444 | font-size: 12px; 445 | } 446 | 447 | iframe { 448 | width: 97% !important; 449 | min-height: 480px; 450 | height: 480px; 451 | } 452 | 453 | 454 | @media (max-width: 900px) { 455 | .angular { 456 | right: 18%; 457 | } 458 | } 459 | @media (max-width: 830px) { 460 | .angular { 461 | right: 12%; 462 | } 463 | .fork-me { 464 | display: none; 465 | } 466 | } 467 | @media (max-width: 700px) { 468 | .angular { 469 | right: 5%; 470 | } 471 | } 472 | @media (max-width: 600px) { 473 | .angular { 474 | display: none; 475 | } 476 | } 477 | -------------------------------------------------------------------------------- /demo/images/angular@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/handsontable/ngHandsontable/310875bd89f16e90c2a668f96d46029d4723dff7/demo/images/angular@2x.png -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Static configuration with dynamic data 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 |
42 |
43 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /demo/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var APP_NAME = 'demos-app', 5 | modules = require('modules'), 6 | routing = require('routing'), 7 | demos = require('demos'), 8 | unique, 9 | app; 10 | 11 | 12 | unique = function(array) { 13 | var unique = [], 14 | len, i; 15 | 16 | for (i = 0, len = array.length; i < len; i++) { 17 | if (unique.indexOf(array[i]) === -1) { 18 | unique.push(array[i]); 19 | } 20 | } 21 | 22 | return unique; 23 | }; 24 | window.App = {}; 25 | window.App.bootstrap = function() { 26 | app = angular.module(APP_NAME, Array.prototype.concat(unique(modules.list), [ 27 | 'ui.router' 28 | ])); 29 | 30 | function config($sceDelegateProvider, $httpProvider, $stateProvider, $compileProvider, $urlRouterProvider, demoMapProvider) { 31 | $sceDelegateProvider.resourceUrlWhitelist(['self']); 32 | $httpProvider.defaults.headers.common['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8'; 33 | $compileProvider.debugInfoEnabled(false); 34 | 35 | demoMapProvider.setDemos(demos); 36 | routing($stateProvider, $urlRouterProvider, demoMapProvider); 37 | } 38 | config.$inject = ['$sceDelegateProvider', '$httpProvider', '$stateProvider', '$compileProvider', '$urlRouterProvider', 'demoMapProvider']; 39 | 40 | app.constant('version', 'v@@version'); 41 | app.config(config); 42 | 43 | angular.element(document).ready(function() { 44 | angular.bootstrap(document, [app.name], { 45 | strictDi: true 46 | }); 47 | }); 48 | }; 49 | }()); 50 | -------------------------------------------------------------------------------- /demo/js/controllers/main-ctrl.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var modules = require('modules'); 5 | 6 | MainCtrl.$inject = ['$state', 'demoMap', 'version']; 7 | 8 | 9 | function MainCtrl($state, demoMap, version) { 10 | this.version = version; 11 | this.allDemos = demoMap.getAll(); 12 | this.selectedDemo = demoMap.get($state.current.name); 13 | } 14 | 15 | 16 | modules('app').register(function(module, path) { 17 | module.controller(path.controller('MainCtrl'), MainCtrl); 18 | }); 19 | }()); 20 | -------------------------------------------------------------------------------- /demo/js/demos.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var 5 | DEFAULT_TABS = 'html,js,output', 6 | BASE_URL = 'http://jsbin.com/{id}/embed?'; 7 | 8 | function getUrl(id, tabs) { 9 | return BASE_URL.replace('{id}', id) + (tabs || DEFAULT_TABS); 10 | } 11 | 12 | module.exports = { 13 | 'intro': { 14 | 'simple-example': { 15 | url: getUrl('nudumu/6'), 16 | title: 'Simple example', 17 | description: 'Simple example' 18 | }, 19 | 'full-featured-demo': { 20 | url: getUrl('xezevi/4'), 21 | title: 'Full featured demo', 22 | description: 'Full featured demo' 23 | } 24 | }, 25 | 'configuration': { 26 | 'configuration-by-object': { 27 | url: getUrl('getazu/4'), 28 | title: 'By `settings` object', 29 | description: 'Configuration by setting `settings` object' 30 | }, 31 | 'configuration-declarative-way': { 32 | url: getUrl('jupeme/4'), 33 | title: 'In declarative way', 34 | description: 'Configuration in declarative way' 35 | } 36 | }, 37 | 'columns': { 38 | 'add-remove-column-ng-repeat': { 39 | url: getUrl('muluto/5'), 40 | title: 'Add/Remove (ng-repeat)', 41 | description: 'Add/Remove columns using ng-repeat' 42 | }, 43 | 'add-remove-column-by-attr': { 44 | url: getUrl('xayeru/4'), 45 | title: 'Add/Remove (`columns` attribute)', 46 | description: 'Add/Remove columns using `columns` attribute' 47 | } 48 | }, 49 | 'binding': { 50 | 'data-binding': { 51 | url: getUrl('lupile/5'), 52 | title: 'Data binding', 53 | description: 'Data binding' 54 | }, 55 | 'settings-binding': { 56 | url: getUrl('xaqasi/4'), 57 | title: 'Table settings binding', 58 | description: 'Table settings binding' 59 | } 60 | }, 61 | 'callbacks': { 62 | 'callbacks-by-object': { 63 | url: getUrl('nayito/7', 'html,js,console,output'), 64 | title: 'By `settings` object', 65 | description: 'Listening callbacks using `settings` object' 66 | }, 67 | 'callbacks-declarative-way': { 68 | url: getUrl('pucale/5', 'html,js,console,output'), 69 | title: 'In declarative way', 70 | description: 'Listening callbacks in declarative way' 71 | } 72 | }, 73 | 'plugins': { 74 | 'copy-paste-context-menu': { 75 | url: getUrl('zohoge/3'), 76 | title: 'Enable copy/paste in context menu', 77 | description: 'Enable copy/paste in context menu' 78 | } 79 | }, 80 | 'pagination': { 81 | 'rows-pagination': { 82 | url: getUrl('rorozo/1'), 83 | title: 'Paginate rows', 84 | description: 'Paginate rows' 85 | }, 86 | 'columns-pagination': { 87 | url: getUrl('kolerig/1'), 88 | title: 'Paginate columns', 89 | description: 'Paginate columns' 90 | }, 91 | }, 92 | 'other': { 93 | 'access-to-instance': { 94 | url: getUrl('fovoxu/5'), 95 | title: 'Access to Handsontable instance', 96 | description: 'Access to Handsontable instance' 97 | }, 98 | 'custom-validator': { 99 | url: getUrl('qoweju/4'), 100 | title: 'Custom validator', 101 | description: 'Custom validator' 102 | }, 103 | 'custom-renderer': { 104 | url: getUrl('locome/3'), 105 | title: 'Custom renderer', 106 | description: 'Custom renderer' 107 | }, 108 | }, 109 | 'PRO': { 110 | 'collapsing-columns-with-nested-headers': { 111 | url: getUrl('wilani/4'), 112 | title: 'Collapsing columns with nested headers', 113 | description: 'Create a nested, hierarchical structure of headers with expandable and collapsible columns' 114 | }, 115 | 'filtering-simple': { 116 | url: getUrl('fijida/4'), 117 | title: 'Data filtering (simple example)', 118 | description: 'Display rows that meet your criteria and hide the rest' 119 | }, 120 | 'filtering-external-inputs': { 121 | url: getUrl('rewefa/10'), 122 | title: 'Data filtering (external inputs)', 123 | description: 'Display rows that meet your criteria and hide the rest' 124 | }, 125 | 'hiding-columns': { 126 | url: getUrl('wapufa/3'), 127 | title: 'Hiding columns', 128 | description: 'Hide specific columns and show the rest' 129 | }, 130 | 'hiding-rows': { 131 | url: getUrl('roximi/4'), 132 | title: 'Hiding rows', 133 | description: 'Hide specific rows and show the rest' 134 | }, 135 | }, 136 | }; 137 | }()); 138 | -------------------------------------------------------------------------------- /demo/js/directives/code-preview.html: -------------------------------------------------------------------------------- 1 |

{{ title }}

2 |

3 | 4 | 5 | -------------------------------------------------------------------------------- /demo/js/directives/code-preview.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var 5 | modules = require('modules'), 6 | 7 | DIRECTIVE_NAME = 'codePreview'; 8 | 9 | CodePreview.$inject = []; 10 | 11 | 12 | function CodePreview() { 13 | return { 14 | restrict: 'EA', 15 | templateUrl: 'js/directives/code-preview.html', 16 | scope: { 17 | title: '=' 18 | }, 19 | link: function(scope, element, attr) { 20 | var iframe = element.find('iframe'); 21 | 22 | iframe[0].src = attr[DIRECTIVE_NAME]; 23 | iframe[0].style.minHeight = (window.innerHeight - iframe[0].getBoundingClientRect().top - 60) + 'px'; 24 | } 25 | }; 26 | } 27 | 28 | 29 | modules('app').register(function(module) { 30 | module.directive(DIRECTIVE_NAME, CodePreview); 31 | }); 32 | }()); 33 | -------------------------------------------------------------------------------- /demo/js/filter/uppercase-first.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var 5 | modules = require('modules'); 6 | 7 | uppercaseFirst.$inject = []; 8 | 9 | 10 | function uppercaseFirst() { 11 | return function(string) { 12 | return string.substr(0, 1).toUpperCase() + string.substr(1, string.length); 13 | }; 14 | } 15 | 16 | 17 | modules('app').register(function(module) { 18 | module.filter('uppercaseFirst', uppercaseFirst); 19 | }); 20 | }()); 21 | -------------------------------------------------------------------------------- /demo/js/modules.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var 5 | /** 6 | * @property _module 7 | * @static 8 | * @private 9 | */ 10 | _module = null, 11 | 12 | /** 13 | * @property _namespace 14 | * @static 15 | * @private 16 | */ 17 | _namespace = '', 18 | 19 | /** 20 | * @property _list 21 | * @static 22 | * @private 23 | * @type {Array} 24 | */ 25 | _list = [], 26 | 27 | Modules; 28 | 29 | 30 | Modules = function Modules(namespace) { 31 | var angularNs; 32 | 33 | if (!(this instanceof Modules)) { 34 | return new Modules(namespace); 35 | } 36 | _namespace = namespace.replace(/\//g, '-'); 37 | _namespace = _namespace.replace(/-\D/g, function(match) { 38 | return match.charAt(1).toUpperCase(); 39 | }); 40 | angularNs = 'modules/' + _namespace; 41 | 42 | if (_list.indexOf(angularNs) === -1) { 43 | _list.push(angularNs); 44 | _module = angular.module(angularNs, []); 45 | } else { 46 | _module = angular.module(angularNs); 47 | } 48 | 49 | return this; 50 | }; 51 | 52 | /** 53 | * @method register 54 | * @param {Array} modules 55 | */ 56 | Modules.prototype.register = function(modules) { 57 | var _this = this; 58 | 59 | if (!angular.isArray(modules)) { 60 | modules = [modules]; 61 | } 62 | angular.forEach(modules, function(module) { 63 | if (!angular.isFunction(module)) { 64 | throw Error('Registered module must be a function. Given ' + angular.identity(module)); 65 | } 66 | module(_module, _this.path()); 67 | }); 68 | }; 69 | 70 | /** 71 | * @method path 72 | * @return {Object} 73 | */ 74 | Modules.prototype.path = function() { 75 | return { 76 | controller: function(name) { 77 | var ctrl = _namespace + name; 78 | 79 | ctrl = ctrl.substr(0, 1).toUpperCase() + ctrl.substr(1, ctrl.length - 1); 80 | 81 | return ctrl; 82 | } 83 | }; 84 | }; 85 | 86 | Modules.list = _list; 87 | 88 | module.exports = Modules; 89 | }()); 90 | -------------------------------------------------------------------------------- /demo/js/routing.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | module.exports = function($stateProvider, $urlRouterProvider, demoMapProvider) { 5 | var map = demoMapProvider.$get().getFlatten(); 6 | 7 | $urlRouterProvider.otherwise('/intro-simple-example'); 8 | 9 | angular.forEach(Object.keys(map), function(key) { 10 | $stateProvider.state(key, { 11 | url: '/' + key, 12 | //templateUrl: 'js/templates/preview.html', 13 | views: { 14 | menu: { 15 | templateUrl: 'templates/menu.html', 16 | controller: 'AppMainCtrl as appCtrl' 17 | }, 18 | preview: { 19 | templateUrl: 'templates/preview.html', 20 | controller: 'AppMainCtrl as appCtrl' 21 | } 22 | } 23 | }); 24 | }); 25 | }; 26 | }()); 27 | -------------------------------------------------------------------------------- /demo/js/services/dataFactory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var 3 | products = [ 4 | { 5 | description: 'Big Mac', 6 | options: [ 7 | {description: 'Big Mac'}, 8 | {description: 'Big Mac & Co'}, 9 | {description: 'McRoyal'}, 10 | {description: 'Hamburger'}, 11 | {description: 'Cheeseburger'}, 12 | {description: 'Double Cheeseburger'} 13 | ] 14 | }, 15 | { 16 | description: 'Fried Potatoes', 17 | options: [ 18 | {description: 'Fried Potatoes'}, 19 | {description: 'Fried Onions'} 20 | ] 21 | } 22 | ], 23 | firstNames = ['Ted', 'John', 'Macy', 'Rob', 'Gwen', 'Fiona', 'Mario', 'Ben', 'Kate', 'Kevin', 'Thomas', 'Frank'], 24 | lastNames = ['Tired', 'Johnson', 'Moore', 'Rocket', 'Goodman', 'Farewell', 'Manson', 'Bentley', 'Kowalski', 'Schmidt', 'Tucker', 'Fancy'], 25 | address = ['Turkey', 'Japan', 'Michigan', 'Russia', 'Greece', 'France', 'USA', 'Germany', 'Sweden', 'Denmark', 'Poland', 'Belgium']; 26 | 27 | function dataFactory() { 28 | return { 29 | generateArrayOfObjects: function(rows, keysToInclude) { 30 | var items = [], item; 31 | 32 | rows = rows || 10; 33 | 34 | for (var i = 0; i < rows; i++) { 35 | item = { 36 | id: i + 1, 37 | name: { 38 | first: firstNames[Math.floor(Math.random() * firstNames.length)], 39 | last: lastNames[Math.floor(Math.random() * lastNames.length)] 40 | }, 41 | date: Math.max(Math.round(Math.random() * 12), 1) + '/' + Math.max(Math.round(Math.random() * 28), 1) + '/' + (Math.round(Math.random() * 80) + 1940), 42 | address: Math.floor(Math.random() * 100000) + ' ' + address[Math.floor(Math.random() * address.length)], 43 | price: Math.floor(Math.random() * 100000) / 100, 44 | isActive: Math.floor(Math.random() * products.length) / 2 === 0 ? 'Yes' : 'No', 45 | product: angular.extend({}, products[Math.floor(Math.random() * products.length)]) 46 | }; 47 | angular.forEach(keysToInclude, function(key) { 48 | if (item[key]) { 49 | delete item[key]; 50 | } 51 | }); 52 | items.push(item); 53 | } 54 | 55 | return items; 56 | }, 57 | 58 | generateArrayOfArrays: function(rows, cols) { 59 | return Handsontable.helper.createSpreadsheetData(rows || 10, cols || 10); 60 | } 61 | }; 62 | } 63 | dataFactory.$inject = []; 64 | 65 | angular.module('ngHandsontable').service('dataFactory', dataFactory); 66 | }()); 67 | -------------------------------------------------------------------------------- /demo/js/services/demoMap.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var 3 | modules = require('modules'), 4 | demos; 5 | 6 | function demoMap() { 7 | this.setDemos = function(_demos) { 8 | demos = _demos; 9 | }; 10 | 11 | this.$get = function() { 12 | return { 13 | get: function(id) { 14 | var splited = id.split('-'); 15 | 16 | return demos[splited[0]] ? demos[splited[0]][splited.splice(1, splited.length).join('-')] : null; 17 | }, 18 | 19 | getAll: function() { 20 | return demos; 21 | }, 22 | 23 | getFlatten: function() { 24 | var result = {}; 25 | 26 | angular.forEach(Object.keys(demos), function(parent) { 27 | angular.forEach(Object.keys(demos[parent]), function(child) { 28 | result[parent + '-' + child] = demos[parent][child]; 29 | }); 30 | }); 31 | 32 | return result; 33 | } 34 | }; 35 | }; 36 | } 37 | demoMap.$inject = []; 38 | 39 | modules('app').register(function(module) { 40 | module.provider('demoMap', demoMap); 41 | }); 42 | }()); 43 | -------------------------------------------------------------------------------- /demo/templates/menu.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /demo/templates/preview.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | -------------------------------------------------------------------------------- /dist/ngHandsontable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ng-handsontable 0.13.2 3 | * 4 | * Copyright 2012-2015 Marcin Warpechowski 5 | * Copyright 2015 Handsoncode sp. z o.o. 6 | * Licensed under the MIT license. 7 | * https://github.com/handsontable/ngHandsontable 8 | * Date: Tue Sep 01 2020 09:23:44 GMT+0200 (czas środkowoeuropejski letni) 9 | */ 10 | 11 | if (document.all && !document.addEventListener) { // IE 8 and lower 12 | document.createElement('hot-table'); 13 | document.createElement('hot-column'); 14 | document.createElement('hot-autocomplete'); 15 | } 16 | 17 | angular.module('ngHandsontable.services', []); 18 | angular.module('ngHandsontable.directives', []); 19 | angular.module('ngHandsontable', [ 20 | 'ngHandsontable.services', 21 | 'ngHandsontable.directives' 22 | ]); 23 | 24 | Handsontable.hooks.add('afterContextMenuShow', function() { 25 | Handsontable.eventManager.isHotTableEnv = false; 26 | }); 27 | 28 | (function() { 29 | function autoCompleteFactory($parse) { 30 | return { 31 | parseAutoComplete: function(column, dataSet, propertyOnly) { 32 | column.source = function(query, process) { 33 | var row = this.instance.getSelected()[0]; 34 | var source = []; 35 | var data = dataSet[row]; 36 | 37 | if (!data) { 38 | return; 39 | } 40 | var options = column.optionList; 41 | 42 | if (!options || !options.object) { 43 | return; 44 | } 45 | if (angular.isArray(options.object)) { 46 | source = options.object; 47 | } else { 48 | // Using $parse to evaluate the expression against the row object 49 | // allows us to support filters like the ngRepeat directive does. 50 | var paramObject = $parse(options.object)(data); 51 | 52 | if (angular.isArray(paramObject)) { 53 | if (propertyOnly) { 54 | for (var i = 0, length = paramObject.length; i < length; i++) { 55 | var item = paramObject[i][options.property]; 56 | 57 | if (item !== null && item !== undefined) { 58 | source.push(item); 59 | } 60 | } 61 | } else { 62 | source = paramObject; 63 | } 64 | } else { 65 | source = paramObject; 66 | } 67 | } 68 | process(source); 69 | }; 70 | } 71 | }; 72 | } 73 | autoCompleteFactory.$inject = ['$parse']; 74 | 75 | angular.module('ngHandsontable.services').factory('autoCompleteFactory', autoCompleteFactory); 76 | }()); 77 | 78 | (function() { 79 | 80 | function hotRegisterer() { 81 | var instances = {}; 82 | 83 | return { 84 | getInstance: function(id) { 85 | return instances[id]; 86 | }, 87 | 88 | registerInstance: function(id, instance) { 89 | instances[id] = instance; 90 | }, 91 | 92 | removeInstance: function(id) { 93 | instances[id] = void 0; 94 | } 95 | }; 96 | } 97 | hotRegisterer.$inject = []; 98 | 99 | angular.module('ngHandsontable.services').factory('hotRegisterer', hotRegisterer); 100 | }()); 101 | 102 | (function() { 103 | 104 | function hyphenate(string) { 105 | return string.replace(/[A-Z]/g, function(match) { 106 | return ('-' + match.charAt(0).toLowerCase()); 107 | }); 108 | } 109 | 110 | function camelCase(string) { 111 | return string.replace(/-\D/g, function(match) { 112 | return match.charAt(1).toUpperCase(); 113 | }); 114 | } 115 | 116 | function ucFirst(string) { 117 | return string.substr(0, 1).toUpperCase() + string.substr(1, string.length - 1); 118 | } 119 | 120 | function settingFactory(hotRegisterer) { 121 | return { 122 | containerClassName: 'handsontable-container', 123 | 124 | /** 125 | * Append handsontable container div and initialize handsontable instance inside element. 126 | * 127 | * @param {qLite} element 128 | * @param {Object} htSettings 129 | */ 130 | initializeHandsontable: function(element, htSettings) { 131 | var container = document.createElement('div'), 132 | hot; 133 | 134 | container.className = this.containerClassName; 135 | 136 | if (htSettings.hotId) { 137 | container.id = htSettings.hotId; 138 | } 139 | element[0].appendChild(container); 140 | hot = new Handsontable(container, htSettings); 141 | 142 | if (htSettings.hotId) { 143 | hotRegisterer.registerInstance(htSettings.hotId, hot); 144 | } 145 | 146 | return hot; 147 | }, 148 | 149 | /** 150 | * Set new settings to handsontable instance. 151 | * 152 | * @param {Handsontable} instance 153 | * @param {Object} settings 154 | */ 155 | updateHandsontableSettings: function(instance, settings) { 156 | if (instance) { 157 | instance.updateSettings(settings); 158 | } 159 | }, 160 | 161 | /** 162 | * Render handsontable instance inside element. 163 | * 164 | * @param {Handsontable} instance 165 | */ 166 | renderHandsontable: function(instance) { 167 | if (instance) { 168 | instance.render(); 169 | } 170 | }, 171 | 172 | /** 173 | * Merge original handsontable settings with setting defined in scope. 174 | * 175 | * @param {Object} settings 176 | * @param {Object} scope 177 | * @returns {Object} 178 | */ 179 | mergeSettingsFromScope: function(settings, scope) { 180 | var 181 | scopeOptions = angular.extend({}, scope), 182 | htOptions, i, length; 183 | 184 | settings = settings || {}; 185 | angular.extend(scopeOptions, scope.settings || {}); 186 | htOptions = this.getAvailableSettings(); 187 | 188 | for (i = 0, length = htOptions.length; i < length; i++) { 189 | if (typeof scopeOptions[htOptions[i]] !== 'undefined') { 190 | settings[htOptions[i]] = scopeOptions[htOptions[i]]; 191 | } 192 | } 193 | 194 | return settings; 195 | }, 196 | 197 | /** 198 | * Merge original handsontable hooks with hooks defined in scope. 199 | * 200 | * @param {Object} settings 201 | * @param {Object} scope 202 | * @returns {Object} 203 | */ 204 | mergeHooksFromScope: function(settings, scope) { 205 | var 206 | scopeOptions = angular.extend({}, scope), 207 | htHooks, i, length, attribute; 208 | 209 | settings = settings || {}; 210 | angular.extend(scopeOptions, scope.settings || {}); 211 | htHooks = this.getAvailableHooks(); 212 | 213 | for (i = 0, length = htHooks.length; i < length; i++) { 214 | attribute = 'on' + ucFirst(htHooks[i]); 215 | 216 | if (typeof scopeOptions[htHooks[i]] === 'function' || typeof scopeOptions[attribute] === 'function') { 217 | settings[htHooks[i]] = scopeOptions[htHooks[i]] || scopeOptions[attribute]; 218 | } 219 | } 220 | 221 | return settings; 222 | }, 223 | 224 | /** 225 | * Trim scope definition according to attrs object from directive. 226 | * 227 | * @param {Object} scopeDefinition 228 | * @param {Object} attrs 229 | * @returns {Object} 230 | */ 231 | trimScopeDefinitionAccordingToAttrs: function(scopeDefinition, attrs) { 232 | for (var i in scopeDefinition) { 233 | if (scopeDefinition.hasOwnProperty(i) && attrs[i] === void 0 && 234 | attrs[scopeDefinition[i].substr(1, scopeDefinition[i].length)] === void 0) { 235 | delete scopeDefinition[i]; 236 | } 237 | } 238 | 239 | return scopeDefinition; 240 | }, 241 | 242 | /** 243 | * Get isolate scope definition for main handsontable directive. 244 | * 245 | * @return {Object} 246 | */ 247 | getTableScopeDefinition: function() { 248 | var scopeDefinition = {}; 249 | 250 | this.applyAvailableSettingsScopeDef(scopeDefinition); 251 | this.applyAvailableHooksScopeDef(scopeDefinition); 252 | 253 | scopeDefinition.datarows = '='; 254 | scopeDefinition.dataschema = '='; 255 | scopeDefinition.observeDomVisibility = '='; 256 | //scopeDefinition.settings = '='; 257 | 258 | return scopeDefinition; 259 | }, 260 | 261 | /** 262 | * Get isolate scope definition for column directive. 263 | * 264 | * @return {Object} 265 | */ 266 | getColumnScopeDefinition: function() { 267 | var scopeDefinition = {}; 268 | 269 | this.applyAvailableSettingsScopeDef(scopeDefinition); 270 | scopeDefinition.data = '@'; 271 | 272 | return scopeDefinition; 273 | }, 274 | 275 | /** 276 | * Apply all available handsontable settings into object which represents scope definition. 277 | * 278 | * @param {Object} [scopeDefinition] 279 | * @returns {Object} 280 | */ 281 | applyAvailableSettingsScopeDef: function(scopeDefinition) { 282 | var options, i, length; 283 | 284 | options = this.getAvailableSettings(); 285 | 286 | for (i = 0, length = options.length; i < length; i++) { 287 | scopeDefinition[options[i]] = '='; 288 | } 289 | 290 | return scopeDefinition; 291 | }, 292 | 293 | /** 294 | * Apply all available handsontable hooks into object which represents scope definition. 295 | * 296 | * @param {Object} [scopeDefinition] 297 | * @returns {Object} 298 | */ 299 | applyAvailableHooksScopeDef: function(scopeDefinition) { 300 | var options, i, length; 301 | 302 | options = this.getAvailableHooks(); 303 | 304 | for (i = 0, length = options.length; i < length; i++) { 305 | scopeDefinition[options[i]] = '=on' + ucFirst(options[i]); 306 | } 307 | 308 | return scopeDefinition; 309 | }, 310 | 311 | /** 312 | * Get all available settings from handsontable, returns settings by default in camelCase mode. 313 | * 314 | * @param {Boolean} [hyphenateStyle=undefined] If `true` then returns options in hyphenate mode (eq. row-header) 315 | * @returns {Array} 316 | */ 317 | getAvailableSettings: function(hyphenateStyle) { 318 | var defaultSettings = Handsontable.DefaultSettings.prototype; 319 | 320 | // For Handsontable v8 the prototype is `undefined`. 321 | if (defaultSettings === void 0) { 322 | defaultSettings = Handsontable.DefaultSettings; 323 | } 324 | 325 | var settings = Object.keys(defaultSettings); 326 | 327 | if (settings.indexOf('contextMenuCopyPaste') === -1) { 328 | settings.push('contextMenuCopyPaste'); 329 | } 330 | if (settings.indexOf('handsontable') === -1) { 331 | settings.push('handsontable'); 332 | } 333 | if (settings.indexOf('settings') >= 0) { 334 | settings.splice(settings.indexOf('settings'), 1); 335 | } 336 | if (hyphenateStyle) { 337 | settings = settings.map(hyphenate); 338 | } 339 | 340 | return settings; 341 | }, 342 | 343 | /** 344 | * Get all available hooks from handsontable, returns hooks by default in camelCase mode. 345 | * 346 | * @param {Boolean} [hyphenateStyle=undefined] If `true` then returns hooks in hyphenate mode (eq. on-after-init) 347 | * @returns {Array} 348 | */ 349 | getAvailableHooks: function(hyphenateStyle) { 350 | var settings = Handsontable.hooks.getRegistered(); 351 | 352 | if (hyphenateStyle) { 353 | settings = settings.map(function(hook) { 354 | return 'on-' + hyphenate(hook); 355 | }); 356 | } 357 | 358 | return settings; 359 | } 360 | }; 361 | } 362 | settingFactory.$inject = ['hotRegisterer']; 363 | 364 | angular.module('ngHandsontable.services').factory('settingFactory', settingFactory); 365 | }()); 366 | 367 | (function() { 368 | /** 369 | * Angular Handsontable directive for autocomplete settings 370 | */ 371 | function hotAutocomplete() { 372 | return { 373 | restrict: 'EA', 374 | scope: true, 375 | require: '^hotColumn', 376 | link: function(scope, element, attrs, controllerInstance) { 377 | var options = attrs.datarows; 378 | 379 | controllerInstance.setColumnOptionList(options); 380 | } 381 | }; 382 | } 383 | hotAutocomplete.$inject = []; 384 | 385 | angular.module('ngHandsontable.directives').directive('hotAutocomplete', hotAutocomplete); 386 | }()); 387 | 388 | (function() { 389 | /** 390 | * Angular Handsontable directive for single column settings 391 | */ 392 | function hotColumn(settingFactory) { 393 | return { 394 | restrict: 'EA', 395 | require: '^hotTable', 396 | scope: {}, 397 | controller: ['$scope', function($scope) { 398 | this.setColumnOptionList = function(options) { 399 | if (!$scope.column) { 400 | $scope.column = {}; 401 | } 402 | var optionList = {}; 403 | var match = options.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)\s*$/); 404 | 405 | if (match) { 406 | optionList.property = match[1]; 407 | optionList.object = match[2]; 408 | } else { 409 | optionList.object = options.split(','); 410 | } 411 | $scope.column.optionList = optionList; 412 | }; 413 | }], 414 | compile: function(tElement, tAttrs) { 415 | var _this = this; 416 | 417 | this.scope = settingFactory.trimScopeDefinitionAccordingToAttrs(settingFactory.getColumnScopeDefinition(), tAttrs); 418 | //this.$$isolateBindings = {}; 419 | 420 | angular.forEach(Object.keys(this.scope), function(key) { 421 | _this.$$isolateBindings[key] = { 422 | attrName: key, 423 | collection: false, 424 | mode: key === 'data' ? '@' : '=', 425 | optional: false 426 | }; 427 | }); 428 | 429 | return function(scope, element, attrs, controllerInstance) { 430 | var column = {}; 431 | 432 | // Turn all attributes without value as `true` by default 433 | angular.forEach(Object.keys(attrs), function(key) { 434 | if (key.charAt(0) !== '$' && attrs[key] === '') { 435 | column[key] = true; 436 | } 437 | }); 438 | settingFactory.mergeSettingsFromScope(column, scope); 439 | 440 | if (!scope.column) { 441 | scope.column = {}; 442 | } 443 | angular.extend(scope.column, column); 444 | controllerInstance.setColumnSetting(scope.column); 445 | 446 | scope.$on('$destroy', function() { 447 | controllerInstance.removeColumnSetting(scope.column); 448 | }); 449 | }; 450 | } 451 | }; 452 | } 453 | hotColumn.$inject = ['settingFactory']; 454 | 455 | angular.module('ngHandsontable.directives').directive('hotColumn', hotColumn); 456 | }()); 457 | 458 | (function() { 459 | /** 460 | * Main Angular Handsontable directive 461 | */ 462 | function hotTable(settingFactory, autoCompleteFactory, $rootScope, $parse) { 463 | return { 464 | restrict: 'EA', 465 | scope: {}, 466 | // for ng-repeat 467 | priority: -400, 468 | controller: ['$scope', function($scope) { 469 | this.setColumnSetting = function(column) { 470 | if (!$scope.htSettings) { 471 | $scope.htSettings = {}; 472 | } 473 | if (!$scope.htSettings.columns) { 474 | $scope.htSettings.columns = []; 475 | } 476 | $scope.htSettings.columns.push(column); 477 | settingFactory.updateHandsontableSettings($scope.hotInstance, $scope.htSettings); 478 | }; 479 | this.removeColumnSetting = function(column) { 480 | if (!$scope.hotInstance) { 481 | return; 482 | } 483 | 484 | if ($scope.htSettings.columns.indexOf(column) > -1) { 485 | $scope.htSettings.columns.splice($scope.htSettings.columns.indexOf(column), 1); 486 | settingFactory.updateHandsontableSettings($scope.hotInstance, $scope.htSettings); 487 | } 488 | }; 489 | }], 490 | compile: function(tElement, tAttrs) { 491 | var _this = this, 492 | bindingsKeys; 493 | 494 | this.scope = settingFactory.trimScopeDefinitionAccordingToAttrs(settingFactory.getTableScopeDefinition(), tAttrs); 495 | bindingsKeys = Object.keys(this.scope); 496 | 497 | angular.forEach(bindingsKeys, function(key) { 498 | var mode = _this.scope[key].charAt(0); 499 | 500 | _this.$$isolateBindings[key] = { 501 | attrName: _this.scope[key].length > 1 ? _this.scope[key].substr(1, _this.scope[key].length) : key, 502 | collection: key === 'datarows', 503 | mode: mode, 504 | optional: false 505 | }; 506 | }); 507 | 508 | return function(scope, element, attrs) { 509 | scope.settings = $parse(attrs.settings)(scope.$parent); 510 | 511 | if (!scope.htSettings) { 512 | scope.htSettings = {}; 513 | } 514 | // Turn all attributes without value as `true` by default 515 | angular.forEach(Object.keys(attrs), function(key) { 516 | if (key.charAt(0) !== '$' && attrs[key] === '') { 517 | scope.htSettings[key] = true; 518 | } 519 | }); 520 | 521 | settingFactory.mergeSettingsFromScope(scope.htSettings, scope); 522 | settingFactory.mergeHooksFromScope(scope.htSettings, scope); 523 | 524 | if (!scope.htSettings.data) { 525 | scope.htSettings.data = scope.datarows; 526 | } 527 | scope.htSettings.dataSchema = scope.dataschema; 528 | scope.htSettings.hotId = attrs.hotId; 529 | scope.htSettings.observeDOMVisibility = scope.observeDomVisibility; 530 | 531 | if (scope.htSettings.columns) { 532 | for (var i = 0, length = scope.htSettings.columns.length; i < length; i++) { 533 | var column = scope.htSettings.columns[i]; 534 | 535 | if (column.type !== 'autocomplete') { 536 | continue; 537 | } 538 | if (!column.optionList) { 539 | continue; 540 | } 541 | if (typeof column.optionList === 'string') { 542 | var optionList = {}; 543 | var match = column.optionList.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)\s*$/); 544 | 545 | if (match) { 546 | optionList.property = match[1]; 547 | optionList.object = match[2]; 548 | } else { 549 | optionList.object = optionList; 550 | } 551 | column.optionList = optionList; 552 | } 553 | autoCompleteFactory.parseAutoComplete(column, scope.datarows, true); 554 | } 555 | } 556 | var origAfterChange = scope.htSettings.afterChange; 557 | 558 | scope.htSettings.afterChange = function() { 559 | if (origAfterChange) { 560 | origAfterChange.apply(this, arguments); 561 | } 562 | if (!$rootScope.$$phase) { 563 | scope.$apply(); 564 | } 565 | }; 566 | scope.hotInstance = settingFactory.initializeHandsontable(element, scope.htSettings); 567 | 568 | // TODO: Add watch properties descriptor + needs perf test. Watch full equality vs toJson 569 | angular.forEach(bindingsKeys, function(key) { 570 | scope.$watch(key, function(newValue, oldValue) { 571 | if (newValue === void 0) { 572 | return; 573 | } 574 | if (key === 'datarows') { 575 | // If reference to data rows is not changed then only re-render table 576 | if (scope.hotInstance.getSettings().data === newValue) { 577 | settingFactory.renderHandsontable(scope.hotInstance); 578 | } else { 579 | scope.hotInstance.loadData(newValue); 580 | scope.htSettings.data = newValue; 581 | } 582 | } else if (newValue !== oldValue) { 583 | scope.htSettings[key] = newValue; 584 | settingFactory.updateHandsontableSettings(scope.hotInstance, scope.htSettings); 585 | } 586 | }, ['datarows', 'columns', 'rowHeights', 'colWidths', 'rowHeaders', 'colHeaders'].indexOf(key) >= 0); 587 | }); 588 | 589 | /** 590 | * Check for reference equality changes for datarows 591 | * TODO: must the remaining bindingsKeys need to be added also if their reference changes 592 | */ 593 | scope.$watch('datarows', function(newValue) { 594 | if (newValue === void 0) { 595 | return; 596 | } 597 | if (scope.hotInstance.getSettings().data !== newValue) { 598 | scope.hotInstance.loadData(newValue); 599 | } 600 | }); 601 | 602 | /** 603 | * Check if data length has been changed 604 | */ 605 | scope.$watchCollection('datarows', function(newValue, oldValue) { 606 | if (oldValue && oldValue.length === scope.htSettings.minSpareRows && newValue.length !== scope.htSettings.minSpareRows) { 607 | scope.htSettings.data = scope.datarows; 608 | settingFactory.updateHandsontableSettings(scope.hotInstance, scope.htSettings); 609 | } 610 | }); 611 | 612 | function destroyInstance() { 613 | if (scope.hotInstance) { 614 | scope.hotInstance.destroy(); 615 | scope.hotInstance = null; 616 | scope.htSettings = {}; 617 | } 618 | } 619 | 620 | // Destroy triggered by controller or scope destroying 621 | scope.$on('$destroy', function() { 622 | destroyInstance(); 623 | }); 624 | 625 | // Destroy triggered by DOM element removing 626 | element.on('$destroy', function() { 627 | destroyInstance(); 628 | }); 629 | }; 630 | } 631 | }; 632 | } 633 | hotTable.$inject = ['settingFactory', 'autoCompleteFactory', '$rootScope', '$parse']; 634 | 635 | angular.module('ngHandsontable.directives').directive('hotTable', hotTable); 636 | }()); 637 | -------------------------------------------------------------------------------- /dist/ngHandsontable.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ng-handsontable 0.13.2 3 | * 4 | * Copyright 2012-2015 Marcin Warpechowski 5 | * Copyright 2015 Handsoncode sp. z o.o. 6 | * Licensed under the MIT license. 7 | * https://github.com/handsontable/ngHandsontable 8 | * Date: Tue Sep 01 2020 09:23:44 GMT+0200 (czas środkowoeuropejski letni) 9 | */ 10 | 11 | document.all&&!document.addEventListener&&(document.createElement("hot-table"),document.createElement("hot-column"),document.createElement("hot-autocomplete")),angular.module("ngHandsontable.services",[]),angular.module("ngHandsontable.directives",[]),angular.module("ngHandsontable",["ngHandsontable.services","ngHandsontable.directives"]),Handsontable.hooks.add("afterContextMenuShow",function(){Handsontable.eventManager.isHotTableEnv=!1}),function(){function a(a){return{parseAutoComplete:function(b,c,d){b.source=function(e,f){var g=this.instance.getSelected()[0],h=[],i=c[g];if(i){var j=b.optionList;if(j&&j.object){if(angular.isArray(j.object))h=j.object;else{var k=a(j.object)(i);if(angular.isArray(k))if(d)for(var l=0,m=k.length;l=0&&d.splice(d.indexOf("settings"),1),b&&(d=d.map(a)),d},getAvailableHooks:function(b){var c=Handsontable.hooks.getRegistered();return b&&(c=c.map(function(b){return"on-"+a(b)})),c}}}c.$inject=["hotRegisterer"],angular.module("ngHandsontable.services").factory("settingFactory",c)}(),function(){function a(){return{restrict:"EA",scope:!0,require:"^hotColumn",link:function(a,b,c,d){var e=c.datarows;d.setColumnOptionList(e)}}}a.$inject=[],angular.module("ngHandsontable.directives").directive("hotAutocomplete",a)}(),function(){function a(a){return{restrict:"EA",require:"^hotTable",scope:{},controller:["$scope",function(a){this.setColumnOptionList=function(b){a.column||(a.column={});var c={},d=b.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)\s*$/);d?(c.property=d[1],c.object=d[2]):c.object=b.split(","),a.column.optionList=c}}],compile:function(b,c){var d=this;return this.scope=a.trimScopeDefinitionAccordingToAttrs(a.getColumnScopeDefinition(),c),angular.forEach(Object.keys(this.scope),function(a){d.$$isolateBindings[a]={attrName:a,collection:!1,mode:"data"===a?"@":"=",optional:!1}}),function(b,c,d,e){var f={};angular.forEach(Object.keys(d),function(a){"$"!==a.charAt(0)&&""===d[a]&&(f[a]=!0)}),a.mergeSettingsFromScope(f,b),b.column||(b.column={}),angular.extend(b.column,f),e.setColumnSetting(b.column),b.$on("$destroy",function(){e.removeColumnSetting(b.column)})}}}}a.$inject=["settingFactory"],angular.module("ngHandsontable.directives").directive("hotColumn",a)}(),function(){function a(a,b,c,d){return{restrict:"EA",scope:{},priority:-400,controller:["$scope",function(b){this.setColumnSetting=function(c){b.htSettings||(b.htSettings={}),b.htSettings.columns||(b.htSettings.columns=[]),b.htSettings.columns.push(c),a.updateHandsontableSettings(b.hotInstance,b.htSettings)},this.removeColumnSetting=function(c){b.hotInstance&&b.htSettings.columns.indexOf(c)>-1&&(b.htSettings.columns.splice(b.htSettings.columns.indexOf(c),1),a.updateHandsontableSettings(b.hotInstance,b.htSettings))}}],compile:function(e,f){var g,h=this;return this.scope=a.trimScopeDefinitionAccordingToAttrs(a.getTableScopeDefinition(),f),g=Object.keys(this.scope),angular.forEach(g,function(a){var b=h.scope[a].charAt(0);h.$$isolateBindings[a]={attrName:h.scope[a].length>1?h.scope[a].substr(1,h.scope[a].length):a,collection:"datarows"===a,mode:b,optional:!1}}),function(e,f,h){function i(){e.hotInstance&&(e.hotInstance.destroy(),e.hotInstance=null,e.htSettings={})}if(e.settings=d(h.settings)(e.$parent),e.htSettings||(e.htSettings={}),angular.forEach(Object.keys(h),function(a){"$"!==a.charAt(0)&&""===h[a]&&(e.htSettings[a]=!0)}),a.mergeSettingsFromScope(e.htSettings,e),a.mergeHooksFromScope(e.htSettings,e),e.htSettings.data||(e.htSettings.data=e.datarows),e.htSettings.dataSchema=e.dataschema,e.htSettings.hotId=h.hotId,e.htSettings.observeDOMVisibility=e.observeDomVisibility,e.htSettings.columns)for(var j=0,k=e.htSettings.columns.length;j=0)}),e.$watch("datarows",function(a){void 0!==a&&e.hotInstance.getSettings().data!==a&&e.hotInstance.loadData(a)}),e.$watchCollection("datarows",function(b,c){c&&c.length===e.htSettings.minSpareRows&&b.length!==e.htSettings.minSpareRows&&(e.htSettings.data=e.datarows,a.updateHandsontableSettings(e.hotInstance,e.htSettings))}),e.$on("$destroy",function(){i()}),f.on("$destroy",function(){i()})}}}}a.$inject=["settingFactory","autoCompleteFactory","$rootScope","$parse"],angular.module("ngHandsontable.directives").directive("hotTable",a)}(); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ngHandsontable.Angular version of Handsontable library 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 |
40 |

ngHandsontable

41 |

Angular wrapper for Handsontable data grid editor

42 | AngularJS 43 |
44 | 45 |

Install

46 |
47 | Install the component using Bower: 48 |

 49 |     $ bower install ngHandsontable --save
 50 |   
51 | using NPM: 52 |

 53 |     $ npm install ng-handsontable --save
 54 |   
55 | Or download as ZIP. 56 | 57 |

Usage

58 |
59 | 1. Include the library files: 60 |

 61 |     <link rel="stylesheet" media="screen" href="bower_components/handsontable/dist/handsontable.full.css">
 62 | 
 63 |     <script src="bower_components/angular/angular.js"></script>
 64 |     <script src="bower_components/handsontable/dist/handsontable.full.js"></script>
 65 |     <script src="bower_components/dist/ngHandsontable.js"></script>
 66 |   
67 | 2. Start using it! 68 |

 69 |     <hot-table datarows="db.items"></hot-table>
 70 |   
71 | 72 |

73 | Note: ngHandsontable supports angular in version 1.3 and greater. 74 |

75 | 76 | 77 | Fork me on GitHub 78 | 79 | 80 |

Simple Example

81 | 82 |
83 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 100 | 101 |
102 | 103 |

Code

104 | 105 |

106 |   <hot-table
107 |     settings="ctrl.settings"
108 |     row-headers="ctrl.rowHeaders"
109 |     min-spare-rows="ctrl.minSpareRows"
110 |     datarows="ctrl.db.items"
111 |     height="300"
112 |     width="700">
113 |       <hot-column data="id" title="'ID'"></hot-column>
114 |       <hot-column data="name.first" title="'First Name'" type="'text'" read-only></hot-column>
115 |       <hot-column data="name.last" title="'Last Name'" read-only></hot-column>
116 |       <hot-column data="address" title="'Address'" width="150"></hot-column>
117 |       <hot-column data="product.description" title="'Favorite food'" type="'autocomplete'">
118 |         <hot-autocomplete datarows="description in product.options"></hot-autocomplete>
119 |       </hot-column>
120 |       <hot-column data="price" title="'Price'" type="'numeric'" width="80" format="'$ 0,0.00'"></hot-column>
121 |       <hot-column data="isActive" title="'Is active'" type="'checkbox'" checked-template="'Yes'"
122 |                   unchecked-template="'No'"></hot-column>
123 |   </hot-table>
124 |   
125 | 126 |

127 | The main directive for ngHandsontable is <hot-table>. To define columns you can use 128 | <hot-column> directive inside <hot-table> directive. 129 | All Handsontable options described in documentation should be supported. 130 |

131 |

132 | You can put all your settings in settings object e.g. settings="{colHeaders: true, contextMenu: true}" attribute or in 133 | separated attributes, e.g. min-spare-rows="minSpareRows", row-headers="false". 134 |

135 |

136 | It's recommended to put all your settings in one big object (settings="ctrl.settings"). 137 | Settings attribute unlike any other attributes is not watched so using this can be helpful to achieve higher performance. 138 |

139 | 140 |

More demos

141 | 151 | 152 |

PRO features

153 |
154 | To install Handsontable PRO overwrite regular handsontable version with new one: 155 |

156 |     $ bower install handsontable=git@git.handsontable.com:handsontable/handsontable-pro.git --save
157 |   
158 | If you don't have license for Pro yet, please click here for more info. 159 | 160 | 161 | 162 | 176 |
177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Mon Jul 13 2015 21:03:33 GMT+0200 (CEST) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['angular', 'jasmine'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'test/phantom-polyfill.js', 19 | 'node_modules/handsontable/dist/handsontable.full.js', 20 | 'dist/ngHandsontable.min.js', 21 | 'test/**/*.spec.js' 22 | ], 23 | 24 | 25 | // list of files to exclude 26 | exclude: [ 27 | ], 28 | 29 | 30 | // preprocess matching files before serving them to the browser 31 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 32 | preprocessors: { 33 | }, 34 | 35 | 36 | // test results reporter to use 37 | // possible values: 'dots', 'progress' 38 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 39 | reporters: ['progress'], 40 | 41 | 42 | // web server port 43 | port: 9876, 44 | 45 | 46 | // enable / disable colors in the output (reporters and logs) 47 | colors: true, 48 | 49 | 50 | // level of logging 51 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 52 | logLevel: config.LOG_INFO, 53 | 54 | 55 | // enable / disable watching file and executing tests whenever any file changes 56 | autoWatch: true, 57 | 58 | 59 | // start these browsers 60 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 61 | browsers: ['PhantomJS', 'Chrome'], 62 | 63 | phantomjsLauncher: { 64 | // Have phantomjs exit if a ResourceError is encountered (useful if karma exits without killing phantom) 65 | exitOnResourceError: true 66 | }, 67 | 68 | // Continuous Integration mode 69 | // if true, Karma captures browsers, runs the tests and exits 70 | singleRun: false 71 | }); 72 | }; 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-handsontable", 3 | "description": "AngularJS directive for Handsontable", 4 | "homepage": "https://github.com/handsontable/ngHandsontable", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/handsontable/ngHandsontable.git" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/handsontable/ngHandsontable/issues" 11 | }, 12 | "author": "Handsoncode ", 13 | "version": "0.13.2", 14 | "dependencies": { 15 | "angular": "^1.5.0", 16 | "handsontable": "^0.28.0" 17 | }, 18 | "devDependencies": { 19 | "angular": "^1.5.0", 20 | "angular-mocks": "^1.5.0", 21 | "angular-ui-router": "^0.2.15", 22 | "browserify-replace": "^0.9.0", 23 | "generate-release": "^0.10.2", 24 | "grunt": "~0.4.5", 25 | "grunt-browserify": "^4.0.0", 26 | "grunt-cli": "^1.3.2", 27 | "grunt-contrib-concat": "^1.0.0", 28 | "grunt-contrib-jshint": "^1.0.0", 29 | "grunt-contrib-uglify": "^0.11.0", 30 | "grunt-contrib-watch": "^0.6.1", 31 | "grunt-jscs": "^2.8.0", 32 | "jasmine-core": "^2.4.0", 33 | "karma": "^0.13.0", 34 | "karma-angular": "^0.0.6", 35 | "karma-chrome-launcher": "^0.2.2", 36 | "karma-jasmine": "^0.3.7", 37 | "karma-phantomjs-launcher": "^1.0.0", 38 | "phantomjs-prebuilt": "^2.1.4" 39 | }, 40 | "scripts": { 41 | "build": "./node_modules/.bin/grunt build-release", 42 | "test": "./node_modules/.bin/karma start --single-run --browsers PhantomJS", 43 | "release": "./node_modules/.bin/generate-release" 44 | }, 45 | "license": "MIT" 46 | } 47 | -------------------------------------------------------------------------------- /src/directives/hotAutocomplete.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | /** 3 | * Angular Handsontable directive for autocomplete settings 4 | */ 5 | function hotAutocomplete() { 6 | return { 7 | restrict: 'EA', 8 | scope: true, 9 | require: '^hotColumn', 10 | link: function(scope, element, attrs, controllerInstance) { 11 | var options = attrs.datarows; 12 | 13 | controllerInstance.setColumnOptionList(options); 14 | } 15 | }; 16 | } 17 | hotAutocomplete.$inject = []; 18 | 19 | angular.module('ngHandsontable.directives').directive('hotAutocomplete', hotAutocomplete); 20 | }()); 21 | -------------------------------------------------------------------------------- /src/directives/hotColumn.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | /** 3 | * Angular Handsontable directive for single column settings 4 | */ 5 | function hotColumn(settingFactory) { 6 | return { 7 | restrict: 'EA', 8 | require: '^hotTable', 9 | scope: {}, 10 | controller: ['$scope', function($scope) { 11 | this.setColumnOptionList = function(options) { 12 | if (!$scope.column) { 13 | $scope.column = {}; 14 | } 15 | var optionList = {}; 16 | var match = options.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)\s*$/); 17 | 18 | if (match) { 19 | optionList.property = match[1]; 20 | optionList.object = match[2]; 21 | } else { 22 | optionList.object = options.split(','); 23 | } 24 | $scope.column.optionList = optionList; 25 | }; 26 | }], 27 | compile: function(tElement, tAttrs) { 28 | var _this = this; 29 | 30 | this.scope = settingFactory.trimScopeDefinitionAccordingToAttrs(settingFactory.getColumnScopeDefinition(), tAttrs); 31 | //this.$$isolateBindings = {}; 32 | 33 | angular.forEach(Object.keys(this.scope), function(key) { 34 | _this.$$isolateBindings[key] = { 35 | attrName: key, 36 | collection: false, 37 | mode: key === 'data' ? '@' : '=', 38 | optional: false 39 | }; 40 | }); 41 | 42 | return function(scope, element, attrs, controllerInstance) { 43 | var column = {}; 44 | 45 | // Turn all attributes without value as `true` by default 46 | angular.forEach(Object.keys(attrs), function(key) { 47 | if (key.charAt(0) !== '$' && attrs[key] === '') { 48 | column[key] = true; 49 | } 50 | }); 51 | settingFactory.mergeSettingsFromScope(column, scope); 52 | 53 | if (!scope.column) { 54 | scope.column = {}; 55 | } 56 | angular.extend(scope.column, column); 57 | controllerInstance.setColumnSetting(scope.column); 58 | 59 | scope.$on('$destroy', function() { 60 | controllerInstance.removeColumnSetting(scope.column); 61 | }); 62 | }; 63 | } 64 | }; 65 | } 66 | hotColumn.$inject = ['settingFactory']; 67 | 68 | angular.module('ngHandsontable.directives').directive('hotColumn', hotColumn); 69 | }()); 70 | -------------------------------------------------------------------------------- /src/directives/hotTable.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | /** 3 | * Main Angular Handsontable directive 4 | */ 5 | function hotTable(settingFactory, autoCompleteFactory, $rootScope, $parse) { 6 | return { 7 | restrict: 'EA', 8 | scope: {}, 9 | // for ng-repeat 10 | priority: -400, 11 | controller: ['$scope', function($scope) { 12 | this.setColumnSetting = function(column) { 13 | if (!$scope.htSettings) { 14 | $scope.htSettings = {}; 15 | } 16 | if (!$scope.htSettings.columns) { 17 | $scope.htSettings.columns = []; 18 | } 19 | $scope.htSettings.columns.push(column); 20 | settingFactory.updateHandsontableSettings($scope.hotInstance, $scope.htSettings); 21 | }; 22 | this.removeColumnSetting = function(column) { 23 | if (!$scope.hotInstance) { 24 | return; 25 | } 26 | 27 | if ($scope.htSettings.columns.indexOf(column) > -1) { 28 | $scope.htSettings.columns.splice($scope.htSettings.columns.indexOf(column), 1); 29 | settingFactory.updateHandsontableSettings($scope.hotInstance, $scope.htSettings); 30 | } 31 | }; 32 | }], 33 | compile: function(tElement, tAttrs) { 34 | var _this = this, 35 | bindingsKeys; 36 | 37 | this.scope = settingFactory.trimScopeDefinitionAccordingToAttrs(settingFactory.getTableScopeDefinition(), tAttrs); 38 | bindingsKeys = Object.keys(this.scope); 39 | 40 | angular.forEach(bindingsKeys, function(key) { 41 | var mode = _this.scope[key].charAt(0); 42 | 43 | _this.$$isolateBindings[key] = { 44 | attrName: _this.scope[key].length > 1 ? _this.scope[key].substr(1, _this.scope[key].length) : key, 45 | collection: key === 'datarows', 46 | mode: mode, 47 | optional: false 48 | }; 49 | }); 50 | 51 | return function(scope, element, attrs) { 52 | scope.settings = $parse(attrs.settings)(scope.$parent); 53 | 54 | if (!scope.htSettings) { 55 | scope.htSettings = {}; 56 | } 57 | // Turn all attributes without value as `true` by default 58 | angular.forEach(Object.keys(attrs), function(key) { 59 | if (key.charAt(0) !== '$' && attrs[key] === '') { 60 | scope.htSettings[key] = true; 61 | } 62 | }); 63 | 64 | settingFactory.mergeSettingsFromScope(scope.htSettings, scope); 65 | settingFactory.mergeHooksFromScope(scope.htSettings, scope); 66 | 67 | if (!scope.htSettings.data) { 68 | scope.htSettings.data = scope.datarows; 69 | } 70 | scope.htSettings.dataSchema = scope.dataschema; 71 | scope.htSettings.hotId = attrs.hotId; 72 | scope.htSettings.observeDOMVisibility = scope.observeDomVisibility; 73 | 74 | if (scope.htSettings.columns) { 75 | for (var i = 0, length = scope.htSettings.columns.length; i < length; i++) { 76 | var column = scope.htSettings.columns[i]; 77 | 78 | if (column.type !== 'autocomplete') { 79 | continue; 80 | } 81 | if (!column.optionList) { 82 | continue; 83 | } 84 | if (typeof column.optionList === 'string') { 85 | var optionList = {}; 86 | var match = column.optionList.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)\s*$/); 87 | 88 | if (match) { 89 | optionList.property = match[1]; 90 | optionList.object = match[2]; 91 | } else { 92 | optionList.object = optionList; 93 | } 94 | column.optionList = optionList; 95 | } 96 | autoCompleteFactory.parseAutoComplete(column, scope.datarows, true); 97 | } 98 | } 99 | var origAfterChange = scope.htSettings.afterChange; 100 | 101 | scope.htSettings.afterChange = function() { 102 | if (origAfterChange) { 103 | origAfterChange.apply(this, arguments); 104 | } 105 | if (!$rootScope.$$phase) { 106 | scope.$apply(); 107 | } 108 | }; 109 | scope.hotInstance = settingFactory.initializeHandsontable(element, scope.htSettings); 110 | 111 | // TODO: Add watch properties descriptor + needs perf test. Watch full equality vs toJson 112 | angular.forEach(bindingsKeys, function(key) { 113 | scope.$watch(key, function(newValue, oldValue) { 114 | if (newValue === void 0) { 115 | return; 116 | } 117 | if (key === 'datarows') { 118 | // If reference to data rows is not changed then only re-render table 119 | if (scope.hotInstance.getSettings().data === newValue) { 120 | settingFactory.renderHandsontable(scope.hotInstance); 121 | } else { 122 | scope.hotInstance.loadData(newValue); 123 | scope.htSettings.data = newValue; 124 | } 125 | } else if (newValue !== oldValue) { 126 | scope.htSettings[key] = newValue; 127 | settingFactory.updateHandsontableSettings(scope.hotInstance, scope.htSettings); 128 | } 129 | }, ['datarows', 'columns', 'rowHeights', 'colWidths', 'rowHeaders', 'colHeaders'].indexOf(key) >= 0); 130 | }); 131 | 132 | /** 133 | * Check for reference equality changes for datarows 134 | * TODO: must the remaining bindingsKeys need to be added also if their reference changes 135 | */ 136 | scope.$watch('datarows', function(newValue) { 137 | if (newValue === void 0) { 138 | return; 139 | } 140 | if (scope.hotInstance.getSettings().data !== newValue) { 141 | scope.hotInstance.loadData(newValue); 142 | } 143 | }); 144 | 145 | /** 146 | * Check if data length has been changed 147 | */ 148 | scope.$watchCollection('datarows', function(newValue, oldValue) { 149 | if (oldValue && oldValue.length === scope.htSettings.minSpareRows && newValue.length !== scope.htSettings.minSpareRows) { 150 | scope.htSettings.data = scope.datarows; 151 | settingFactory.updateHandsontableSettings(scope.hotInstance, scope.htSettings); 152 | } 153 | }); 154 | 155 | function destroyInstance() { 156 | if (scope.hotInstance) { 157 | scope.hotInstance.destroy(); 158 | scope.hotInstance = null; 159 | scope.htSettings = {}; 160 | } 161 | } 162 | 163 | // Destroy triggered by controller or scope destroying 164 | scope.$on('$destroy', function() { 165 | destroyInstance(); 166 | }); 167 | 168 | // Destroy triggered by DOM element removing 169 | element.on('$destroy', function() { 170 | destroyInstance(); 171 | }); 172 | }; 173 | } 174 | }; 175 | } 176 | hotTable.$inject = ['settingFactory', 'autoCompleteFactory', '$rootScope', '$parse']; 177 | 178 | angular.module('ngHandsontable.directives').directive('hotTable', hotTable); 179 | }()); 180 | -------------------------------------------------------------------------------- /src/ie-shim.js: -------------------------------------------------------------------------------- 1 | if (document.all && !document.addEventListener) { // IE 8 and lower 2 | document.createElement('hot-table'); 3 | document.createElement('hot-column'); 4 | document.createElement('hot-autocomplete'); 5 | } 6 | -------------------------------------------------------------------------------- /src/ngHandsontable.js: -------------------------------------------------------------------------------- 1 | angular.module('ngHandsontable.services', []); 2 | angular.module('ngHandsontable.directives', []); 3 | angular.module('ngHandsontable', [ 4 | 'ngHandsontable.services', 5 | 'ngHandsontable.directives' 6 | ]); 7 | 8 | Handsontable.hooks.add('afterContextMenuShow', function() { 9 | Handsontable.eventManager.isHotTableEnv = false; 10 | }); 11 | -------------------------------------------------------------------------------- /src/services/autoCompleteFactory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | function autoCompleteFactory($parse) { 3 | return { 4 | parseAutoComplete: function(column, dataSet, propertyOnly) { 5 | column.source = function(query, process) { 6 | var row = this.instance.getSelected()[0]; 7 | var source = []; 8 | var data = dataSet[row]; 9 | 10 | if (!data) { 11 | return; 12 | } 13 | var options = column.optionList; 14 | 15 | if (!options || !options.object) { 16 | return; 17 | } 18 | if (angular.isArray(options.object)) { 19 | source = options.object; 20 | } else { 21 | // Using $parse to evaluate the expression against the row object 22 | // allows us to support filters like the ngRepeat directive does. 23 | var paramObject = $parse(options.object)(data); 24 | 25 | if (angular.isArray(paramObject)) { 26 | if (propertyOnly) { 27 | for (var i = 0, length = paramObject.length; i < length; i++) { 28 | var item = paramObject[i][options.property]; 29 | 30 | if (item !== null && item !== undefined) { 31 | source.push(item); 32 | } 33 | } 34 | } else { 35 | source = paramObject; 36 | } 37 | } else { 38 | source = paramObject; 39 | } 40 | } 41 | process(source); 42 | }; 43 | } 44 | }; 45 | } 46 | autoCompleteFactory.$inject = ['$parse']; 47 | 48 | angular.module('ngHandsontable.services').factory('autoCompleteFactory', autoCompleteFactory); 49 | }()); 50 | -------------------------------------------------------------------------------- /src/services/hotRegisterer.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | function hotRegisterer() { 4 | var instances = {}; 5 | 6 | return { 7 | getInstance: function(id) { 8 | return instances[id]; 9 | }, 10 | 11 | registerInstance: function(id, instance) { 12 | instances[id] = instance; 13 | }, 14 | 15 | removeInstance: function(id) { 16 | instances[id] = void 0; 17 | } 18 | }; 19 | } 20 | hotRegisterer.$inject = []; 21 | 22 | angular.module('ngHandsontable.services').factory('hotRegisterer', hotRegisterer); 23 | }()); 24 | -------------------------------------------------------------------------------- /src/services/settingFactory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | function hyphenate(string) { 4 | return string.replace(/[A-Z]/g, function(match) { 5 | return ('-' + match.charAt(0).toLowerCase()); 6 | }); 7 | } 8 | 9 | function camelCase(string) { 10 | return string.replace(/-\D/g, function(match) { 11 | return match.charAt(1).toUpperCase(); 12 | }); 13 | } 14 | 15 | function ucFirst(string) { 16 | return string.substr(0, 1).toUpperCase() + string.substr(1, string.length - 1); 17 | } 18 | 19 | function settingFactory(hotRegisterer) { 20 | return { 21 | containerClassName: 'handsontable-container', 22 | 23 | /** 24 | * Append handsontable container div and initialize handsontable instance inside element. 25 | * 26 | * @param {qLite} element 27 | * @param {Object} htSettings 28 | */ 29 | initializeHandsontable: function(element, htSettings) { 30 | var container = document.createElement('div'), 31 | hot; 32 | 33 | container.className = this.containerClassName; 34 | 35 | if (htSettings.hotId) { 36 | container.id = htSettings.hotId; 37 | } 38 | element[0].appendChild(container); 39 | hot = new Handsontable(container, htSettings); 40 | 41 | if (htSettings.hotId) { 42 | hotRegisterer.registerInstance(htSettings.hotId, hot); 43 | } 44 | 45 | return hot; 46 | }, 47 | 48 | /** 49 | * Set new settings to handsontable instance. 50 | * 51 | * @param {Handsontable} instance 52 | * @param {Object} settings 53 | */ 54 | updateHandsontableSettings: function(instance, settings) { 55 | if (instance) { 56 | instance.updateSettings(settings); 57 | } 58 | }, 59 | 60 | /** 61 | * Render handsontable instance inside element. 62 | * 63 | * @param {Handsontable} instance 64 | */ 65 | renderHandsontable: function(instance) { 66 | if (instance) { 67 | instance.render(); 68 | } 69 | }, 70 | 71 | /** 72 | * Merge original handsontable settings with setting defined in scope. 73 | * 74 | * @param {Object} settings 75 | * @param {Object} scope 76 | * @returns {Object} 77 | */ 78 | mergeSettingsFromScope: function(settings, scope) { 79 | var 80 | scopeOptions = angular.extend({}, scope), 81 | htOptions, i, length; 82 | 83 | settings = settings || {}; 84 | angular.extend(scopeOptions, scope.settings || {}); 85 | htOptions = this.getAvailableSettings(); 86 | 87 | for (i = 0, length = htOptions.length; i < length; i++) { 88 | if (typeof scopeOptions[htOptions[i]] !== 'undefined') { 89 | settings[htOptions[i]] = scopeOptions[htOptions[i]]; 90 | } 91 | } 92 | 93 | return settings; 94 | }, 95 | 96 | /** 97 | * Merge original handsontable hooks with hooks defined in scope. 98 | * 99 | * @param {Object} settings 100 | * @param {Object} scope 101 | * @returns {Object} 102 | */ 103 | mergeHooksFromScope: function(settings, scope) { 104 | var 105 | scopeOptions = angular.extend({}, scope), 106 | htHooks, i, length, attribute; 107 | 108 | settings = settings || {}; 109 | angular.extend(scopeOptions, scope.settings || {}); 110 | htHooks = this.getAvailableHooks(); 111 | 112 | for (i = 0, length = htHooks.length; i < length; i++) { 113 | attribute = 'on' + ucFirst(htHooks[i]); 114 | 115 | if (typeof scopeOptions[htHooks[i]] === 'function' || typeof scopeOptions[attribute] === 'function') { 116 | settings[htHooks[i]] = scopeOptions[htHooks[i]] || scopeOptions[attribute]; 117 | } 118 | } 119 | 120 | return settings; 121 | }, 122 | 123 | /** 124 | * Trim scope definition according to attrs object from directive. 125 | * 126 | * @param {Object} scopeDefinition 127 | * @param {Object} attrs 128 | * @returns {Object} 129 | */ 130 | trimScopeDefinitionAccordingToAttrs: function(scopeDefinition, attrs) { 131 | for (var i in scopeDefinition) { 132 | if (scopeDefinition.hasOwnProperty(i) && attrs[i] === void 0 && 133 | attrs[scopeDefinition[i].substr(1, scopeDefinition[i].length)] === void 0) { 134 | delete scopeDefinition[i]; 135 | } 136 | } 137 | 138 | return scopeDefinition; 139 | }, 140 | 141 | /** 142 | * Get isolate scope definition for main handsontable directive. 143 | * 144 | * @return {Object} 145 | */ 146 | getTableScopeDefinition: function() { 147 | var scopeDefinition = {}; 148 | 149 | this.applyAvailableSettingsScopeDef(scopeDefinition); 150 | this.applyAvailableHooksScopeDef(scopeDefinition); 151 | 152 | scopeDefinition.datarows = '='; 153 | scopeDefinition.dataschema = '='; 154 | scopeDefinition.observeDomVisibility = '='; 155 | //scopeDefinition.settings = '='; 156 | 157 | return scopeDefinition; 158 | }, 159 | 160 | /** 161 | * Get isolate scope definition for column directive. 162 | * 163 | * @return {Object} 164 | */ 165 | getColumnScopeDefinition: function() { 166 | var scopeDefinition = {}; 167 | 168 | this.applyAvailableSettingsScopeDef(scopeDefinition); 169 | scopeDefinition.data = '@'; 170 | 171 | return scopeDefinition; 172 | }, 173 | 174 | /** 175 | * Apply all available handsontable settings into object which represents scope definition. 176 | * 177 | * @param {Object} [scopeDefinition] 178 | * @returns {Object} 179 | */ 180 | applyAvailableSettingsScopeDef: function(scopeDefinition) { 181 | var options, i, length; 182 | 183 | options = this.getAvailableSettings(); 184 | 185 | for (i = 0, length = options.length; i < length; i++) { 186 | scopeDefinition[options[i]] = '='; 187 | } 188 | 189 | return scopeDefinition; 190 | }, 191 | 192 | /** 193 | * Apply all available handsontable hooks into object which represents scope definition. 194 | * 195 | * @param {Object} [scopeDefinition] 196 | * @returns {Object} 197 | */ 198 | applyAvailableHooksScopeDef: function(scopeDefinition) { 199 | var options, i, length; 200 | 201 | options = this.getAvailableHooks(); 202 | 203 | for (i = 0, length = options.length; i < length; i++) { 204 | scopeDefinition[options[i]] = '=on' + ucFirst(options[i]); 205 | } 206 | 207 | return scopeDefinition; 208 | }, 209 | 210 | /** 211 | * Get all available settings from handsontable, returns settings by default in camelCase mode. 212 | * 213 | * @param {Boolean} [hyphenateStyle=undefined] If `true` then returns options in hyphenate mode (eq. row-header) 214 | * @returns {Array} 215 | */ 216 | getAvailableSettings: function(hyphenateStyle) { 217 | var defaultSettings = Handsontable.DefaultSettings.prototype; 218 | 219 | // For Handsontable v8 the prototype is `undefined`. 220 | if (defaultSettings === void 0) { 221 | defaultSettings = Handsontable.DefaultSettings; 222 | } 223 | 224 | var settings = Object.keys(defaultSettings); 225 | 226 | if (settings.indexOf('contextMenuCopyPaste') === -1) { 227 | settings.push('contextMenuCopyPaste'); 228 | } 229 | if (settings.indexOf('handsontable') === -1) { 230 | settings.push('handsontable'); 231 | } 232 | if (settings.indexOf('settings') >= 0) { 233 | settings.splice(settings.indexOf('settings'), 1); 234 | } 235 | if (hyphenateStyle) { 236 | settings = settings.map(hyphenate); 237 | } 238 | 239 | return settings; 240 | }, 241 | 242 | /** 243 | * Get all available hooks from handsontable, returns hooks by default in camelCase mode. 244 | * 245 | * @param {Boolean} [hyphenateStyle=undefined] If `true` then returns hooks in hyphenate mode (eq. on-after-init) 246 | * @returns {Array} 247 | */ 248 | getAvailableHooks: function(hyphenateStyle) { 249 | var settings = Handsontable.hooks.getRegistered(); 250 | 251 | if (hyphenateStyle) { 252 | settings = settings.map(function(hook) { 253 | return 'on-' + hyphenate(hook); 254 | }); 255 | } 256 | 257 | return settings; 258 | } 259 | }; 260 | } 261 | settingFactory.$inject = ['hotRegisterer']; 262 | 263 | angular.module('ngHandsontable.services').factory('settingFactory', settingFactory); 264 | }()); 265 | -------------------------------------------------------------------------------- /test/directives/hotAutocomplete.spec.js: -------------------------------------------------------------------------------- 1 | 2 | describe('hotAutocomplete', function() { 3 | var rootScope, compile; 4 | 5 | beforeEach(module('ngHandsontable')); 6 | 7 | beforeEach(inject(function(_$compile_, _$rootScope_, $q) { 8 | rootScope = _$rootScope_; 9 | compile = _$compile_; 10 | })); 11 | 12 | afterEach(function() { 13 | angular.element(document.querySelector('hot-table')).remove(); 14 | }); 15 | 16 | it('should create Handsontable with autocomplete column cells (as array items)', function() { 17 | rootScope.list = ['A', 'B', 'C'].join(','); 18 | var scope = angular.element(compile( 19 | '' + 20 | '' + 21 | '' + 22 | '' + 23 | '' 24 | )(rootScope)).isolateScope(); 25 | 26 | scope.$digest(); 27 | 28 | expect(scope.hotInstance.getSettings().columns[0].optionList.object).toEqual(['A', 'B', 'C']); 29 | }); 30 | 31 | it('should create Handsontable with autocomplete column cells (with properties)', function() { 32 | rootScope.list = [{desc: 'Foo'}, {desc: 'Bar'}, {desc: 'Baz'}]; 33 | var scope = angular.element(compile( 34 | '' + 35 | '' + 36 | '' + 37 | '' + 38 | '' 39 | )(rootScope)).isolateScope(); 40 | 41 | scope.$digest(); 42 | 43 | expect(scope.hotInstance.getSettings().columns[0].optionList).toEqual({property: 'desc', object: 'lists'}); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/directives/hotColumn.spec.js: -------------------------------------------------------------------------------- 1 | 2 | describe('hotColumn', function() { 3 | var rootScope, compile; 4 | 5 | beforeEach(module('ngHandsontable')); 6 | 7 | beforeEach(inject(function(_$compile_, _$rootScope_, $q) { 8 | rootScope = _$rootScope_; 9 | compile = _$compile_; 10 | })); 11 | 12 | afterEach(function() { 13 | angular.element(document.querySelector('hot-table')).remove(); 14 | }); 15 | 16 | it('should create Handsontable with 3 columns', function() { 17 | var scope = angular.element(compile( 18 | '' + 19 | '' + 20 | '' + 21 | '' + 22 | '' 23 | )(rootScope)).isolateScope(); 24 | 25 | scope.$digest(); 26 | 27 | expect(scope.hotInstance.getSettings().columns.length).toBe(3); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/directives/hotColumn/watchingOptions.spec.js: -------------------------------------------------------------------------------- 1 | 2 | describe('hotColumn', function() { 3 | var rootScope, compile; 4 | 5 | beforeEach(module('ngHandsontable')); 6 | 7 | beforeEach(inject(function(_$compile_, _$rootScope_, $q) { 8 | rootScope = _$rootScope_; 9 | compile = _$compile_; 10 | })); 11 | 12 | afterEach(function() { 13 | angular.element(document.querySelector('hot-table')).remove(); 14 | }); 15 | 16 | it('should create table with `width` attribute in columns', function() { 17 | rootScope.columns = [void 0, 100, 120]; 18 | var scope = angular.element(compile( 19 | '' + 20 | '' + 21 | '' + 22 | '' + 23 | '' 24 | )(rootScope)).isolateScope(); 25 | 26 | scope.$digest(); 27 | var columns = scope.hotInstance.getSettings().columns; 28 | 29 | expect(columns[0].width).toBe(void 0); 30 | expect(columns[1].width).toBe(100); 31 | expect(columns[2].width).toBe(120); 32 | }); 33 | 34 | it('should create table with `title` attribute in columns', function() { 35 | rootScope.columns = [void 0, 'foo', 'bar']; 36 | var scope = angular.element(compile( 37 | '' + 38 | '' + 39 | '' + 40 | '' + 41 | '' 42 | )(rootScope)).isolateScope(); 43 | 44 | scope.$digest(); 45 | 46 | var columns = scope.hotInstance.getSettings().columns; 47 | 48 | expect(columns[0].title).toBe(void 0); 49 | expect(columns[1].title).toBe('foo'); 50 | expect(columns[2].title).toBe('bar'); 51 | }); 52 | 53 | it('should create table with `data` attribute in columns', function() { 54 | rootScope.data = [{id: 4, name: 'John', gender: 'male'}]; 55 | var scope = angular.element(compile( 56 | '' + 57 | '' + 58 | '' + 59 | '' + 60 | '' 61 | )(rootScope)).isolateScope(); 62 | 63 | scope.$digest(); 64 | var columns = scope.hotInstance.getSettings().columns; 65 | 66 | expect(columns[0].data).toBe('id'); 67 | expect(columns[1].data).toBe('name'); 68 | expect(columns[2].data).toBe('gender'); 69 | }); 70 | 71 | it('should create table with `type` attribute in columns', function() { 72 | rootScope.types = ['text', 'numeric', 'date']; 73 | var scope = angular.element(compile( 74 | '' + 75 | '' + 76 | '' + 77 | '' + 78 | '' 79 | )(rootScope)).isolateScope(); 80 | 81 | scope.$digest(); 82 | var columns = scope.hotInstance.getSettings().columns; 83 | 84 | expect(columns[0].type).toBe('text'); 85 | expect(columns[1].type).toBe('numeric'); 86 | expect(columns[2].type).toBe('date'); 87 | }); 88 | 89 | it('should create table with `readOnly` attribute in columns', function() { 90 | rootScope.readOnly = true; 91 | var scope = angular.element(compile( 92 | '' + 93 | '' + 94 | '' + 95 | '' + 96 | '' 97 | )(rootScope)).isolateScope(); 98 | 99 | scope.$digest(); 100 | var columns = scope.hotInstance.getSettings().columns; 101 | 102 | expect(columns[0].readOnly).toBe(void 0); 103 | expect(columns[1].readOnly).toBe(true); 104 | expect(columns[2].readOnly).toBe(true); 105 | }); 106 | 107 | it('should create table with `format` attribute in columns', function() { 108 | rootScope.formats = [void 0, '$0', '0.0USD']; 109 | var scope = angular.element(compile( 110 | '' + 111 | '' + 112 | '' + 113 | '' + 114 | '' 115 | )(rootScope)).isolateScope(); 116 | 117 | scope.$digest(); 118 | var columns = scope.hotInstance.getSettings().columns; 119 | 120 | expect(columns[0].format).toBe(void 0); 121 | expect(columns[1].format).toBe('$0'); 122 | expect(columns[2].format).toBe('0.0USD'); 123 | }); 124 | 125 | it('should create table with `checkedTemplate` attribute in columns', function() { 126 | rootScope.checkedTemplate = [true, 'Yes', 'Go']; 127 | var scope = angular.element(compile( 128 | '' + 129 | '' + 130 | '' + 131 | '' + 132 | '' 133 | )(rootScope)).isolateScope(); 134 | 135 | scope.$digest(); 136 | var columns = scope.hotInstance.getSettings().columns; 137 | 138 | expect(columns[0].checkedTemplate).toBe(true); 139 | expect(columns[1].checkedTemplate).toBe('Yes'); 140 | expect(columns[2].checkedTemplate).toBe('Go'); 141 | }); 142 | 143 | it('should create table with `uncheckedTemplate` attribute in columns', function() { 144 | rootScope.uncheckedTemplate = [false, 'No', 'Stop']; 145 | var scope = angular.element(compile( 146 | '' + 147 | '' + 148 | '' + 149 | '' + 150 | '' 151 | )(rootScope)).isolateScope(); 152 | 153 | scope.$digest(); 154 | var columns = scope.hotInstance.getSettings().columns; 155 | 156 | expect(columns[0].uncheckedTemplate).toBe(false); 157 | expect(columns[1].uncheckedTemplate).toBe('No'); 158 | expect(columns[2].uncheckedTemplate).toBe('Stop'); 159 | }); 160 | 161 | it('should create table with `renderer` attribute in columns', function() { 162 | rootScope.renderer = [function(){}, function(){}]; 163 | var scope = angular.element(compile( 164 | '' + 165 | '' + 166 | '' + 167 | '' 168 | )(rootScope)).isolateScope(); 169 | 170 | scope.$digest(); 171 | var columns = scope.hotInstance.getSettings().columns; 172 | 173 | expect(columns[0].renderer).toBe(rootScope.renderer[0]); 174 | expect(columns[1].renderer).toBe(rootScope.renderer[1]); 175 | }); 176 | 177 | it('should create table with `validator` attribute in columns', function() { 178 | rootScope.validator = [/[0-9]/, function(){return true;}]; 179 | var scope = angular.element(compile( 180 | '' + 181 | '' + 182 | '' + 183 | '' 184 | )(rootScope)).isolateScope(); 185 | 186 | scope.$digest(); 187 | var columns = scope.hotInstance.getSettings().columns; 188 | 189 | expect(columns[0].validator).toBe(rootScope.validator[0]); 190 | expect(columns[1].validator()).toBe(true); 191 | }); 192 | 193 | it('should create table with `handsontable` attribute in columns', function() { 194 | rootScope.handsontable = {foo: 'bar'}; 195 | var scope = angular.element(compile( 196 | '' + 197 | '' + 198 | '' 199 | )(rootScope)).isolateScope(); 200 | 201 | scope.$digest(); 202 | var columns = scope.hotInstance.getSettings().columns; 203 | 204 | expect(columns[0].handsontable).toBe(rootScope.handsontable); 205 | }); 206 | }); 207 | -------------------------------------------------------------------------------- /test/directives/hotTable.spec.js: -------------------------------------------------------------------------------- 1 | 2 | describe('hotTable', function() { 3 | var rootScope, compile; 4 | 5 | beforeEach(module('ngHandsontable')); 6 | 7 | beforeEach(inject(function(_$compile_, _$rootScope_, $q, hotRegisterer) { 8 | rootScope = _$rootScope_; 9 | compile = _$compile_; 10 | _hotRegisterer = hotRegisterer; 11 | })); 12 | 13 | afterEach(function() { 14 | angular.element(document.querySelector('hot-table')).remove(); 15 | }); 16 | 17 | it('should create the Handsontable table', function() { 18 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 19 | 20 | scope.$digest(); 21 | 22 | expect(scope.hotInstance).toBeDefined(); 23 | }); 24 | 25 | it('should destroy the Handsontable table when the scope is destroyed', function() { 26 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 27 | 28 | scope.$destroy(); 29 | 30 | expect(scope.hotInstance).toBeNull(); 31 | }); 32 | 33 | it('should destroy the Handsontable table when the DOM element is removed', function() { 34 | var element = angular.element(compile('')(rootScope)); 35 | var scope = element.isolateScope(); 36 | 37 | element.remove(); 38 | 39 | expect(scope.hotInstance).toBeNull(); 40 | }); 41 | 42 | it('should re-render table after datarows change', function(done) { 43 | var afterRenderSpy = jasmine.createSpyObj('afterRender', ['callback']); 44 | rootScope.value = [['foo']]; 45 | var element = angular.element(compile('')(rootScope)); 46 | 47 | angular.element(document.body).append(element); 48 | rootScope.$digest(); 49 | 50 | _hotRegisterer.getInstance('hot').addHook('afterRender', afterRenderSpy.callback); 51 | 52 | setTimeout(function() { 53 | rootScope.value[0][0] = 'bar'; 54 | rootScope.$digest(); 55 | 56 | expect(afterRenderSpy.callback).toHaveBeenCalled(); 57 | done(); 58 | }, 100); 59 | }); 60 | 61 | it('should re-render table after columns change', function(done) { 62 | var afterRenderSpy = jasmine.createSpyObj('callback', ['callback']); 63 | rootScope.value = [{width: 10, type: 'date'}, {type: 'numeric', format: '$0'}]; 64 | var element = angular.element(compile('')(rootScope)); 65 | 66 | angular.element(document.body).append(element); 67 | rootScope.$digest(); 68 | 69 | _hotRegisterer.getInstance('hot').addHook('afterRender', afterRenderSpy.callback); 70 | 71 | setTimeout(function() { 72 | rootScope.value.push({type: 'text'}); 73 | rootScope.$digest(); 74 | 75 | expect(afterRenderSpy.callback).toHaveBeenCalled(); 76 | done(); 77 | }, 100); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /test/directives/hotTable/watchingHooks.spec.js: -------------------------------------------------------------------------------- 1 | 2 | describe('hotTable - Attribute options watch', function() { 3 | var rootScope, compile; 4 | 5 | beforeEach(module('ngHandsontable')); 6 | 7 | beforeEach(inject(function(_$compile_, _$rootScope_, $q, hotRegisterer) { 8 | rootScope = _$rootScope_; 9 | compile = _$compile_; 10 | _hotRegisterer = hotRegisterer; 11 | })); 12 | 13 | afterEach(function() { 14 | angular.element(document.querySelector('hot-table')).remove(); 15 | }); 16 | 17 | it('every hook defined in settings should be replaced by hook defined in attribute', function() { 18 | rootScope.value = function() {}; 19 | rootScope.value2 = function() {}; 20 | 21 | spyOn(rootScope, 'value').and.callThrough(); 22 | spyOn(rootScope, 'value2').and.callThrough(); 23 | 24 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 25 | 26 | scope.$digest(); 27 | 28 | expect(rootScope.value).not.toHaveBeenCalled(); 29 | expect(rootScope.value2).toHaveBeenCalled(); 30 | }); 31 | 32 | it('should call `afterChange` hook once after data changed (defined in settings object)', function() { 33 | rootScope.value = function() {}; 34 | spyOn(rootScope, 'value').and.callThrough(); 35 | 36 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 37 | 38 | scope.$digest(); 39 | 40 | expect(rootScope.value).toHaveBeenCalled(); 41 | expect(rootScope.value.calls.count()).toBe(1); 42 | 43 | scope.hotInstance.setDataAtCell(0, 0, ''); 44 | 45 | expect(rootScope.value.calls.count()).toBe(2); 46 | }); 47 | 48 | it('should call `afterChange` hook once after data changed (defined in attribute)', function() { 49 | rootScope.value = function() {}; 50 | spyOn(rootScope, 'value').and.callThrough(); 51 | 52 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 53 | 54 | scope.$digest(); 55 | 56 | expect(rootScope.value).toHaveBeenCalled(); 57 | expect(rootScope.value.calls.count()).toBe(1); 58 | 59 | scope.hotInstance.setDataAtCell(0, 0, ''); 60 | 61 | expect(rootScope.value.calls.count()).toBe(2); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/directives/hotTable/watchingOptions.spec.js: -------------------------------------------------------------------------------- 1 | 2 | describe('hotTable - Watching options', function() { 3 | var rootScope, compile; 4 | 5 | beforeEach(module('ngHandsontable')); 6 | 7 | beforeEach(inject(function(_$compile_, _$rootScope_, $q, hotRegisterer) { 8 | rootScope = _$rootScope_; 9 | compile = _$compile_; 10 | _hotRegisterer = hotRegisterer; 11 | })); 12 | 13 | afterEach(function() { 14 | angular.element(document.querySelector('hot-table')).remove(); 15 | }); 16 | 17 | it('should create table with `datarows` attribute', function() { 18 | rootScope.value = [[]]; 19 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 20 | 21 | scope.$digest(); 22 | 23 | expect(scope.hotInstance.getSourceData()).toBe(rootScope.value); 24 | expect(scope.hotInstance.getData()).toEqual([[null]]); 25 | }); 26 | 27 | it('should ensure a new copy of `datarows` is made when the reference is changed but it has the same value', function() { 28 | rootScope.value = [[1,2,3,4]]; 29 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 30 | 31 | scope.$digest(); 32 | 33 | expect(scope.hotInstance.getSettings().data).toBe(rootScope.value); 34 | 35 | rootScope.value = [[1, 2, 3, 4]]; 36 | rootScope.$digest(); 37 | 38 | expect(scope.hotInstance.getSettings().data).toBe(rootScope.value); 39 | 40 | rootScope.value.push([[5, 6, 7, 8]]); 41 | scope.$digest(); 42 | 43 | expect(scope.hotInstance.getSettings().data).toBe(rootScope.value); 44 | }); 45 | 46 | it('should create table with `dataSchema` attribute', function() { 47 | rootScope.value = {id: null}; 48 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 49 | 50 | scope.$digest(); 51 | 52 | expect(scope.hotInstance.getSchema()).toBe(rootScope.value); 53 | }); 54 | 55 | it('should create table with `width` attribute', function() { 56 | rootScope.value = 333; 57 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 58 | 59 | scope.$digest(); 60 | 61 | expect(scope.hotInstance.getSettings().width).toBe(rootScope.value); 62 | }); 63 | 64 | it('should create table with `height` attribute', function() { 65 | rootScope.value = 333; 66 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 67 | 68 | scope.$digest(); 69 | 70 | expect(scope.hotInstance.getSettings().height).toBe(rootScope.value); 71 | }); 72 | 73 | it('should create table with `startRows` attribute', function() { 74 | rootScope.value = 12; 75 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 76 | 77 | scope.$digest(); 78 | 79 | expect(scope.hotInstance.getSettings().startRows).toBe(rootScope.value); 80 | }); 81 | 82 | it('should create table with `startCols` attribute', function() { 83 | rootScope.value = 12; 84 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 85 | 86 | scope.$digest(); 87 | 88 | expect(scope.hotInstance.getSettings().startCols).toBe(rootScope.value); 89 | }); 90 | 91 | it('should create table with `rowHeaders` attribute', function() { 92 | rootScope.value = function(index){return index + 'A'}; 93 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 94 | 95 | scope.$digest(); 96 | 97 | expect(scope.hotInstance.getSettings().rowHeaders(1)).toBe('1A'); 98 | }); 99 | 100 | it('should create table with `colHeaders` attribute', function() { 101 | rootScope.value = 'AAA'; 102 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 103 | 104 | scope.$digest(); 105 | 106 | expect(scope.hotInstance.getSettings().colHeaders).toBe('AAA'); 107 | }); 108 | 109 | it('should create table with `colWidths` attribute', function() { 110 | rootScope.value = [10, 20, 30]; 111 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 112 | 113 | scope.$digest(); 114 | 115 | expect(scope.hotInstance.getSettings().colWidths).toBe(rootScope.value); 116 | }); 117 | 118 | it('should create table with `columns` attribute', function() { 119 | rootScope.value = [{width: 10, type: 'date'}, {type: 'numeric', format: '$0'}]; 120 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 121 | 122 | scope.$digest(); 123 | 124 | expect(scope.hotInstance.getSettings().columns).toBe(rootScope.value); 125 | }); 126 | 127 | it('should create table with `cells` attribute', function() { 128 | rootScope.value = function(row, col, prop) { return [row, col, prop].join('|') }; 129 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 130 | 131 | scope.$digest(); 132 | 133 | expect(scope.hotInstance.getSettings().cells(1, 2, 3)).toBe('1|2|3'); 134 | }); 135 | 136 | it('should create table with `cell` attribute', function() { 137 | rootScope.value = function(row, col, prop) { return [row, col, prop].join('|') }; 138 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 139 | 140 | scope.$digest(); 141 | 142 | expect(scope.hotInstance.getSettings().cell(1, 2, 3)).toBe('1|2|3'); 143 | }); 144 | 145 | it('should create table with `comments` attribute', function() { 146 | rootScope.value = [{row: 1, col: 1, comment: 'Test'}]; 147 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 148 | 149 | scope.$digest(); 150 | 151 | expect(scope.hotInstance.getSettings().comments).toBe(rootScope.value); 152 | }); 153 | 154 | it('should create table with `customBorders` attribute', function() { 155 | rootScope.value = [{range: { 156 | from: {row: 1, col: 1}, 157 | to: {row: 3, col: 4}}, 158 | left: {}, 159 | right: {}, 160 | top: {}, 161 | bottom: {} 162 | }]; 163 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 164 | 165 | scope.$digest(); 166 | 167 | expect(scope.hotInstance.getSettings().customBorders).toBe(rootScope.value); 168 | }); 169 | 170 | it('should create table with `minRows` attribute', function() { 171 | rootScope.value = 16; 172 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 173 | 174 | scope.$digest(); 175 | 176 | expect(scope.hotInstance.getSettings().minRows).toBe(rootScope.value); 177 | }); 178 | 179 | it('should create table with `minCols` attribute', function() { 180 | rootScope.value = 16; 181 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 182 | 183 | scope.$digest(); 184 | 185 | expect(scope.hotInstance.getSettings().minCols).toBe(rootScope.value); 186 | }); 187 | 188 | it('should create table with `maxRows` attribute', function() { 189 | rootScope.value = 16; 190 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 191 | 192 | scope.$digest(); 193 | 194 | expect(scope.hotInstance.getSettings().maxRows).toBe(rootScope.value); 195 | }); 196 | 197 | it('should create table with `maxCols` attribute', function() { 198 | rootScope.value = 16; 199 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 200 | 201 | scope.$digest(); 202 | 203 | expect(scope.hotInstance.getSettings().maxCols).toBe(rootScope.value); 204 | }); 205 | 206 | it('should create table with `minSpareRows` attribute', function() { 207 | rootScope.value = 16; 208 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 209 | 210 | scope.$digest(); 211 | 212 | expect(scope.hotInstance.getSettings().minSpareRows).toBe(rootScope.value); 213 | }); 214 | 215 | it('should create table with `minSpareCols` attribute', function() { 216 | rootScope.value = 16; 217 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 218 | 219 | scope.$digest(); 220 | 221 | expect(scope.hotInstance.getSettings().minSpareCols).toBe(rootScope.value); 222 | }); 223 | 224 | it('should create table with `allowInsertRow` attribute', function() { 225 | rootScope.value = false; 226 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 227 | 228 | scope.$digest(); 229 | 230 | expect(scope.hotInstance.getSettings().allowInsertRow).toBe(rootScope.value); 231 | }); 232 | 233 | it('should create table with `allowInsertColumn` attribute', function() { 234 | rootScope.value = false; 235 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 236 | 237 | scope.$digest(); 238 | 239 | expect(scope.hotInstance.getSettings().allowInsertColumn).toBe(rootScope.value); 240 | }); 241 | 242 | it('should create table with `allowRemoveRow` attribute', function() { 243 | rootScope.value = false; 244 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 245 | 246 | scope.$digest(); 247 | 248 | expect(scope.hotInstance.getSettings().allowRemoveRow).toBe(rootScope.value); 249 | }); 250 | 251 | it('should create table with `allowRemoveColumn` attribute', function() { 252 | rootScope.value = false; 253 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 254 | 255 | scope.$digest(); 256 | 257 | expect(scope.hotInstance.getSettings().allowRemoveColumn).toBe(rootScope.value); 258 | }); 259 | 260 | it('should create table with `multiSelect` attribute', function() { 261 | rootScope.value = false; 262 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 263 | 264 | scope.$digest(); 265 | 266 | expect(scope.hotInstance.getSettings().multiSelect).toBe(rootScope.value); 267 | }); 268 | 269 | it('should create table with `multiSelect` attribute', function() { 270 | rootScope.value = false; 271 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 272 | 273 | scope.$digest(); 274 | 275 | expect(scope.hotInstance.getSettings().multiSelect).toBe(rootScope.value); 276 | }); 277 | 278 | it('should create table with `fillHandle` attribute', function() { 279 | rootScope.value = false; 280 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 281 | 282 | scope.$digest(); 283 | 284 | expect(scope.hotInstance.getSettings().fillHandle).toBe(rootScope.value); 285 | }); 286 | 287 | it('should create table with `fixedRowsTop` attribute', function() { 288 | rootScope.value = 12; 289 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 290 | 291 | scope.$digest(); 292 | 293 | expect(scope.hotInstance.getSettings().fixedRowsTop).toBe(rootScope.value); 294 | }); 295 | 296 | it('should create table with `fixedColumnsLeft` attribute', function() { 297 | rootScope.value = 12; 298 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 299 | 300 | scope.$digest(); 301 | 302 | expect(scope.hotInstance.getSettings().fixedColumnsLeft).toBe(rootScope.value); 303 | }); 304 | 305 | it('should create table with `outsideClickDeselects` attribute', function() { 306 | rootScope.value = false; 307 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 308 | 309 | scope.$digest(); 310 | 311 | expect(scope.hotInstance.getSettings().outsideClickDeselects).toBe(rootScope.value); 312 | }); 313 | 314 | it('should create table with `enterBeginsEditing` attribute', function() { 315 | rootScope.value = false; 316 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 317 | 318 | scope.$digest(); 319 | 320 | expect(scope.hotInstance.getSettings().enterBeginsEditing).toBe(rootScope.value); 321 | }); 322 | 323 | it('should create table with `enterMoves` attribute', function() { 324 | rootScope.value = {row: 3, col: 2}; 325 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 326 | 327 | scope.$digest(); 328 | 329 | expect(scope.hotInstance.getSettings().enterMoves).toBe(rootScope.value); 330 | }); 331 | 332 | it('should create table with `tabMoves` attribute', function() { 333 | rootScope.value = {row: 3, col: 2}; 334 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 335 | 336 | scope.$digest(); 337 | 338 | expect(scope.hotInstance.getSettings().tabMoves).toBe(rootScope.value); 339 | }); 340 | 341 | it('should create table with `autoWrapRow` attribute', function() { 342 | rootScope.value = true; 343 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 344 | 345 | scope.$digest(); 346 | 347 | expect(scope.hotInstance.getSettings().autoWrapRow).toBe(rootScope.value); 348 | }); 349 | 350 | it('should create table with `autoWrapCol` attribute', function() { 351 | rootScope.value = true; 352 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 353 | 354 | scope.$digest(); 355 | 356 | expect(scope.hotInstance.getSettings().autoWrapCol).toBe(rootScope.value); 357 | }); 358 | 359 | it('should create table with `copyRowsLimit` attribute', function() { 360 | rootScope.value = 563; 361 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 362 | 363 | scope.$digest(); 364 | 365 | expect(scope.hotInstance.getSettings().copyRowsLimit).toBe(rootScope.value); 366 | }); 367 | 368 | it('should create table with `copyColsLimit` attribute', function() { 369 | rootScope.value = 563; 370 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 371 | 372 | scope.$digest(); 373 | 374 | expect(scope.hotInstance.getSettings().copyColsLimit).toBe(rootScope.value); 375 | }); 376 | 377 | it('should create table with `pasteMode` attribute', function() { 378 | rootScope.value = 'test'; 379 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 380 | 381 | scope.$digest(); 382 | 383 | expect(scope.hotInstance.getSettings().pasteMode).toBe(rootScope.value); 384 | }); 385 | 386 | it('should create table with `persistentState` attribute', function() { 387 | rootScope.value = true; 388 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 389 | 390 | scope.$digest(); 391 | 392 | expect(scope.hotInstance.getSettings().persistentState).toBe(rootScope.value); 393 | }); 394 | 395 | it('should create table with `currentRowClassName` attribute', function() { 396 | rootScope.value = 'class-name'; 397 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 398 | 399 | scope.$digest(); 400 | 401 | expect(scope.hotInstance.getSettings().currentRowClassName).toBe(rootScope.value); 402 | }); 403 | 404 | it('should create table with `currentColClassName` attribute', function() { 405 | rootScope.value = 'class-name'; 406 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 407 | 408 | scope.$digest(); 409 | 410 | expect(scope.hotInstance.getSettings().currentColClassName).toBe(rootScope.value); 411 | }); 412 | 413 | it('should create table with `stretchH` attribute', function() { 414 | rootScope.value = 'all'; 415 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 416 | 417 | scope.$digest(); 418 | 419 | expect(scope.hotInstance.getSettings().stretchH).toBe(rootScope.value); 420 | }); 421 | 422 | it('should create table with `isEmptyRow` attribute', function() { 423 | rootScope.value = function(a) {return a;}; 424 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 425 | 426 | scope.$digest(); 427 | 428 | expect(scope.hotInstance.getSettings().isEmptyRow(2)).toBe(2); 429 | }); 430 | 431 | it('should create table with `isEmptyCol` attribute', function() { 432 | rootScope.value = function(a) {return a;}; 433 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 434 | 435 | scope.$digest(); 436 | 437 | expect(scope.hotInstance.getSettings().isEmptyCol(2)).toBe(2); 438 | }); 439 | 440 | it('should create table with `observeDOMVisibility` attribute', function() { 441 | rootScope.value = false; 442 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 443 | 444 | scope.$digest(); 445 | 446 | expect(scope.hotInstance.getSettings().observeDOMVisibility).toBe(rootScope.value); 447 | }); 448 | 449 | it('should create table with `allowInvalid` attribute', function() { 450 | rootScope.value = false; 451 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 452 | 453 | scope.$digest(); 454 | 455 | expect(scope.hotInstance.getSettings().allowInvalid).toBe(rootScope.value); 456 | }); 457 | 458 | it('should create table with `invalidCellClassName` attribute', function() { 459 | rootScope.value = 'class-name'; 460 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 461 | 462 | scope.$digest(); 463 | 464 | expect(scope.hotInstance.getSettings().invalidCellClassName).toBe(rootScope.value); 465 | }); 466 | 467 | it('should create table with `placeholder` attribute', function() { 468 | rootScope.value = true; 469 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 470 | 471 | scope.$digest(); 472 | 473 | expect(scope.hotInstance.getSettings().placeholder).toBe(rootScope.value); 474 | }); 475 | 476 | it('should create table with `placeholderCellClassName` attribute', function() { 477 | rootScope.value = 'class-name'; 478 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 479 | 480 | scope.$digest(); 481 | 482 | expect(scope.hotInstance.getSettings().placeholderCellClassName).toBe(rootScope.value); 483 | }); 484 | 485 | it('should create table with `readOnlyCellClassName` attribute', function() { 486 | rootScope.value = 'class-name'; 487 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 488 | 489 | scope.$digest(); 490 | 491 | expect(scope.hotInstance.getSettings().readOnlyCellClassName).toBe(rootScope.value); 492 | }); 493 | 494 | it('should create table with `renderer` attribute', function() { 495 | rootScope.value = 'numeric'; 496 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 497 | 498 | scope.$digest(); 499 | 500 | expect(scope.hotInstance.getSettings().renderer).toBe(rootScope.value); 501 | }); 502 | 503 | it('should create table with `commentedCellClassName` attribute', function() { 504 | rootScope.value = 'class-name'; 505 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 506 | 507 | scope.$digest(); 508 | 509 | expect(scope.hotInstance.getSettings().commentedCellClassName).toBe(rootScope.value); 510 | }); 511 | 512 | it('should create table with `fragmentSelection` attribute', function() { 513 | rootScope.value = true; 514 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 515 | 516 | scope.$digest(); 517 | 518 | expect(scope.hotInstance.getSettings().fragmentSelection).toBe(rootScope.value); 519 | }); 520 | 521 | it('should create table with `readOnly` attribute', function() { 522 | rootScope.value = true; 523 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 524 | 525 | scope.$digest(); 526 | 527 | expect(scope.hotInstance.getSettings().readOnly).toBe(rootScope.value); 528 | }); 529 | 530 | it('should create table with `search` attribute', function() { 531 | rootScope.value = true; 532 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 533 | 534 | scope.$digest(); 535 | 536 | expect(scope.hotInstance.getSettings().search).toBe(rootScope.value); 537 | }); 538 | 539 | it('should create table with `type` attribute', function() { 540 | rootScope.value = 'date'; 541 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 542 | 543 | scope.$digest(); 544 | 545 | expect(scope.hotInstance.getSettings().type).toBe(rootScope.value); 546 | }); 547 | 548 | it('should create table with `copyable` attribute', function() { 549 | rootScope.value = false; 550 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 551 | 552 | scope.$digest(); 553 | 554 | expect(scope.hotInstance.getSettings().copyable).toBe(rootScope.value); 555 | }); 556 | 557 | it('should create table with `editor` attribute', function() { 558 | rootScope.value = 'date'; 559 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 560 | 561 | scope.$digest(); 562 | 563 | expect(scope.hotInstance.getSettings().editor).toBe(rootScope.value); 564 | }); 565 | 566 | it('should create table with `autoComplete` attribute', function() { 567 | rootScope.value = true; 568 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 569 | 570 | scope.$digest(); 571 | 572 | expect(scope.hotInstance.getSettings().autoComplete).toBe(rootScope.value); 573 | }); 574 | 575 | it('should create table with `debug` attribute', function() { 576 | rootScope.value = true; 577 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 578 | 579 | scope.$digest(); 580 | 581 | expect(scope.hotInstance.getSettings().debug).toBe(rootScope.value); 582 | }); 583 | 584 | it('should create table with `wordWrap` attribute', function() { 585 | rootScope.value = false; 586 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 587 | 588 | scope.$digest(); 589 | 590 | expect(scope.hotInstance.getSettings().wordWrap).toBe(rootScope.value); 591 | }); 592 | 593 | it('should create table with `noWordWrapClassName` attribute', function() { 594 | rootScope.value = 'class-name'; 595 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 596 | 597 | scope.$digest(); 598 | 599 | expect(scope.hotInstance.getSettings().noWordWrapClassName).toBe(rootScope.value); 600 | }); 601 | 602 | it('should create table with `contextMenu` attribute', function() { 603 | rootScope.value = true; 604 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 605 | 606 | scope.$digest(); 607 | 608 | expect(scope.hotInstance.getSettings().contextMenu).toBe(rootScope.value); 609 | }); 610 | 611 | it('should create table with `undo` attribute', function() { 612 | rootScope.value = true; 613 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 614 | 615 | scope.$digest(); 616 | 617 | expect(scope.hotInstance.getSettings().undo).toBe(rootScope.value); 618 | }); 619 | 620 | it('should create table with `columnSorting` attribute', function() { 621 | rootScope.value = true; 622 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 623 | 624 | scope.$digest(); 625 | 626 | expect(scope.hotInstance.getSettings().columnSorting).toBe(rootScope.value); 627 | }); 628 | 629 | it('should create table with `manualColumnMove` attribute', function() { 630 | rootScope.value = true; 631 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 632 | 633 | scope.$digest(); 634 | 635 | expect(scope.hotInstance.getSettings().manualColumnMove).toBe(rootScope.value); 636 | }); 637 | 638 | it('should create table with `manualColumnResize` attribute', function() { 639 | rootScope.value = true; 640 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 641 | 642 | scope.$digest(); 643 | 644 | expect(scope.hotInstance.getSettings().manualColumnResize).toBe(rootScope.value); 645 | }); 646 | 647 | it('should create table with `manualRowMove` attribute', function() { 648 | rootScope.value = true; 649 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 650 | 651 | scope.$digest(); 652 | 653 | expect(scope.hotInstance.getSettings().manualRowMove).toBe(rootScope.value); 654 | }); 655 | 656 | it('should create table with `manualRowResize` attribute', function() { 657 | rootScope.value = true; 658 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 659 | 660 | scope.$digest(); 661 | 662 | expect(scope.hotInstance.getSettings().manualRowResize).toBe(rootScope.value); 663 | }); 664 | 665 | it('should create table with `mergeCells` attribute', function() { 666 | rootScope.value = true; 667 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 668 | 669 | scope.$digest(); 670 | 671 | expect(scope.hotInstance.getSettings().mergeCells).toBe(rootScope.value); 672 | }); 673 | 674 | it('should create table with `viewportRowRenderingOffset` attribute', function() { 675 | rootScope.value = 7; 676 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 677 | 678 | scope.$digest(); 679 | 680 | expect(scope.hotInstance.getSettings().viewportRowRenderingOffset).toBe(rootScope.value); 681 | }); 682 | 683 | it('should create table with `viewportColumnRenderingOffset` attribute', function() { 684 | rootScope.value = 7; 685 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 686 | 687 | scope.$digest(); 688 | 689 | expect(scope.hotInstance.getSettings().viewportColumnRenderingOffset).toBe(rootScope.value); 690 | }); 691 | 692 | //it('should create table with `groups` attribute', function() { 693 | // rootScope.value = true; 694 | // var scope = angular.element(compile('')(rootScope)).isolateScope(); 695 | // 696 | // scope.$digest(); 697 | // 698 | // expect(scope.hotInstance.getSettings().groups).toBe(rootScope.value); 699 | //}); 700 | 701 | it('should create table with `validator` attribute', function() { 702 | rootScope.value = function(a) { return a === 1; }; 703 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 704 | 705 | scope.$digest(); 706 | 707 | expect(scope.hotInstance.getSettings().validator(0)).toBe(false); 708 | expect(scope.hotInstance.getSettings().validator(1)).toBe(true); 709 | }); 710 | 711 | it('should create table with `disableVisualSelection` attribute', function() { 712 | rootScope.value = true; 713 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 714 | 715 | scope.$digest(); 716 | 717 | expect(scope.hotInstance.getSettings().disableVisualSelection).toBe(rootScope.value); 718 | }); 719 | 720 | it('should create table with `sortIndicator` attribute', function() { 721 | rootScope.value = true; 722 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 723 | 724 | scope.$digest(); 725 | 726 | expect(scope.hotInstance.getSettings().sortIndicator).toBe(rootScope.value); 727 | }); 728 | 729 | it('should create table with `manualColumnFreeze` attribute', function() { 730 | rootScope.value = true; 731 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 732 | 733 | scope.$digest(); 734 | 735 | expect(scope.hotInstance.getSettings().manualColumnFreeze).toBe(rootScope.value); 736 | }); 737 | 738 | it('should create table with `trimWhitespace` attribute', function() { 739 | rootScope.value = false; 740 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 741 | 742 | scope.$digest(); 743 | 744 | expect(scope.hotInstance.getSettings().trimWhitespace).toBe(rootScope.value); 745 | }); 746 | 747 | it('should create table with `settings` attribute', function() { 748 | rootScope.value = {width: 100, trimWhitespace: false, sortIndicator: true, columns: [{type: 'date'}]}; 749 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 750 | 751 | scope.$digest(); 752 | 753 | expect(scope.hotInstance.getSettings().width).toBe(rootScope.value.width); 754 | expect(scope.hotInstance.getSettings().trimWhitespace).toBe(rootScope.value.trimWhitespace); 755 | expect(scope.hotInstance.getSettings().sortIndicator).toBe(rootScope.value.sortIndicator); 756 | expect(scope.hotInstance.getSettings().columns).toBe(rootScope.value.columns); 757 | }); 758 | 759 | it('should create table with `source` attribute', function() { 760 | rootScope.value = ['a', 'b']; 761 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 762 | 763 | scope.$digest(); 764 | 765 | expect(scope.hotInstance.getSettings().source).toBe(rootScope.value); 766 | }); 767 | 768 | it('should create table with `title` attribute', function() { 769 | rootScope.value = 'some title'; 770 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 771 | 772 | scope.$digest(); 773 | 774 | expect(scope.hotInstance.getSettings().title).toBe(rootScope.value); 775 | }); 776 | 777 | it('should create table with `checkedTemplate` attribute', function() { 778 | rootScope.value = 1; 779 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 780 | 781 | scope.$digest(); 782 | 783 | expect(scope.hotInstance.getSettings().checkedTemplate).toBe(rootScope.value); 784 | }); 785 | 786 | it('should create table with `uncheckedTemplate` attribute', function() { 787 | rootScope.value = -1; 788 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 789 | 790 | scope.$digest(); 791 | 792 | expect(scope.hotInstance.getSettings().uncheckedTemplate).toBe(rootScope.value); 793 | }); 794 | 795 | it('should create table with `format` attribute', function() { 796 | rootScope.value = '$0,0.0'; 797 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 798 | 799 | scope.$digest(); 800 | 801 | expect(scope.hotInstance.getSettings().format).toBe(rootScope.value); 802 | }); 803 | 804 | it('should create table with `className` attribute', function() { 805 | rootScope.value = 'class-name'; 806 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 807 | 808 | scope.$digest(); 809 | 810 | expect(scope.hotInstance.getSettings().className).toBe(rootScope.value); 811 | }); 812 | 813 | it('should create table with `autoColumnSize` attribute', function() { 814 | rootScope.value = true; 815 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 816 | 817 | scope.$digest(); 818 | 819 | expect(scope.hotInstance.getSettings().autoColumnSize).toBe(rootScope.value); 820 | }); 821 | 822 | it('should create table with `autoRowSize` attribute', function() { 823 | rootScope.value = true; 824 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 825 | 826 | scope.$digest(); 827 | 828 | expect(scope.hotInstance.getSettings().autoRowSize).toBe(rootScope.value); 829 | }); 830 | 831 | it('should convert attributes without values as `true`', function() { 832 | rootScope.value = true; 833 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 834 | 835 | scope.$digest(); 836 | 837 | expect(scope.hotInstance.getSettings().rowHeaders).toBe(true); 838 | expect(scope.hotInstance.getSettings().readOnly).toBe(true); 839 | }); 840 | 841 | it('shouldn\'t observe `settings` object changes', function() { 842 | rootScope.settings = {className: 'foo', rowHeaders: true, data: [[1]]}; 843 | var scope = angular.element(compile('')(rootScope)).isolateScope(); 844 | 845 | rootScope.settings.rowHeaders = false; 846 | rootScope.settings.className = 'bar'; 847 | rootScope.settings.data = [[2]]; 848 | scope.$digest(); 849 | 850 | expect(scope.hotInstance.getSettings().rowHeaders).toBe(true); 851 | expect(scope.hotInstance.getSettings().className).toBe('foo'); 852 | expect(scope.hotInstance.getSettings().data).toEqual([[1]]); 853 | }); 854 | }); 855 | -------------------------------------------------------------------------------- /test/phantom-polyfill.js: -------------------------------------------------------------------------------- 1 | /** 2 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind 3 | */ 4 | if (!Function.prototype.bind) { 5 | Function.prototype.bind = function (oThis) { 6 | if (typeof this !== "function") { 7 | // closest thing possible to the ECMAScript 5 internal IsCallable function 8 | throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); 9 | } 10 | var aArgs = Array.prototype.slice.call(arguments, 1), 11 | fToBind = this, 12 | FNOP = function () {}, 13 | fBound = function () { 14 | return fToBind.apply(this instanceof FNOP && oThis ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments))); 15 | }; 16 | 17 | FNOP.prototype = this.prototype; 18 | fBound.prototype = new FNOP(); 19 | 20 | return fBound; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /test/services/autoCompleteFactory.spec.js: -------------------------------------------------------------------------------- 1 | 2 | describe('settingFactory', function() { 3 | 4 | beforeEach(module('ngHandsontable.services')); 5 | 6 | it('should have defined property container className', inject(function(autoCompleteFactory) { 7 | expect(autoCompleteFactory).toBeDefined(); 8 | })); 9 | 10 | it('should create column source function', inject(function(autoCompleteFactory) { 11 | var column = {}; 12 | var dataSet = []; 13 | 14 | autoCompleteFactory.parseAutoComplete(column, dataSet); 15 | 16 | expect(column.source).toBeDefined(); 17 | })); 18 | 19 | it('should create column source function and shouldn\'t process when data not exists', inject(function(autoCompleteFactory) { 20 | var instance = {getSelected: function() {}}; 21 | var process = jasmine.createSpy('process'); 22 | var data = [0]; 23 | var column = {optionList: []}; 24 | var dataSet = []; 25 | 26 | column.instance = instance; 27 | spyOn(instance, 'getSelected').and.returnValue(data); 28 | 29 | autoCompleteFactory.parseAutoComplete(column, dataSet); 30 | 31 | column.source(null, process); 32 | 33 | expect(instance.getSelected).toHaveBeenCalled(); 34 | expect(process).not.toHaveBeenCalled(); 35 | })); 36 | 37 | it('should create column source function and shouldn\'t process when optionList not exists', inject(function(autoCompleteFactory) { 38 | var instance = {getSelected: function() {}}; 39 | var process = jasmine.createSpy('process'); 40 | var data = [0]; 41 | var columnOptions = {object: false}; 42 | var column = {optionList: columnOptions}; 43 | var dataSet = [{}]; 44 | 45 | column.instance = instance; 46 | spyOn(instance, 'getSelected').and.returnValue(data); 47 | 48 | autoCompleteFactory.parseAutoComplete(column, dataSet); 49 | 50 | column.source(null, process); 51 | 52 | expect(instance.getSelected).toHaveBeenCalled(); 53 | expect(process).not.toHaveBeenCalled(); 54 | })); 55 | 56 | it('should create column source function and should call process callback when column object property is array', inject(function(autoCompleteFactory) { 57 | var instance = {getSelected: function() {}}; 58 | var process = jasmine.createSpy('process'); 59 | var data = [0]; 60 | var columnOptions = {object: [1, 2, 3, 4, 5]}; 61 | var column = {optionList: columnOptions}; 62 | var dataSet = [{}]; 63 | 64 | column.instance = instance; 65 | spyOn(instance, 'getSelected').and.returnValue(data); 66 | 67 | autoCompleteFactory.parseAutoComplete(column, dataSet); 68 | 69 | column.source(null, process); 70 | 71 | expect(instance.getSelected).toHaveBeenCalled(); 72 | expect(process).toHaveBeenCalledWith([1, 2, 3, 4, 5]); 73 | })); 74 | 75 | it('should create column source function and should call process callback when column object property is string', inject(function(autoCompleteFactory) { 76 | var instance = {getSelected: function() {}}; 77 | var process = jasmine.createSpy('process'); 78 | var data = [0]; 79 | var columnOptions = {object: 'a.b.c'}; 80 | var column = {optionList: columnOptions}; 81 | var dataSet = [{a: {b: {c: 'test value'}}}]; 82 | 83 | column.instance = instance; 84 | spyOn(instance, 'getSelected').and.returnValue(data); 85 | 86 | autoCompleteFactory.parseAutoComplete(column, dataSet); 87 | 88 | column.source(null, process); 89 | 90 | expect(instance.getSelected).toHaveBeenCalled(); 91 | expect(process).toHaveBeenCalledWith('test value'); 92 | })); 93 | 94 | it('should create column source function and should call process callback when column object property is string and propertyOnly argument is `true`', inject(function(autoCompleteFactory) { 95 | var instance = {getSelected: function() {}}; 96 | var process = jasmine.createSpy('process'); 97 | var data = [0]; 98 | var columnOptions = {object: 'a.b.c', property: 'myProperty'}; 99 | var column = {optionList: columnOptions}; 100 | var dataSet = [{a: {b: {c: [{myProperty: 1}, {myProperty: 'test'}]}}}]; 101 | 102 | column.instance = instance; 103 | spyOn(instance, 'getSelected').and.returnValue(data); 104 | 105 | autoCompleteFactory.parseAutoComplete(column, dataSet, true); 106 | 107 | column.source(null, process); 108 | 109 | expect(instance.getSelected).toHaveBeenCalled(); 110 | expect(process).toHaveBeenCalledWith([1, 'test']); 111 | })); 112 | }); 113 | -------------------------------------------------------------------------------- /test/services/hotRegisterer.spec.js: -------------------------------------------------------------------------------- 1 | 2 | describe('hotRegisterer', function() { 3 | 4 | beforeEach(module('ngHandsontable.services')); 5 | 6 | it('should be defined', inject(function(hotRegisterer) { 7 | expect(hotRegisterer).toBeDefined(); 8 | })); 9 | 10 | it('should register Handsontable instance', inject(function(hotRegisterer) { 11 | var instanceMock = {}; 12 | hotRegisterer.registerInstance('foo', instanceMock); 13 | 14 | expect(hotRegisterer.getInstance('foo')).toBe(instanceMock); 15 | })); 16 | 17 | it('should overwrite by registered Handsontable instance of the same key', inject(function(hotRegisterer) { 18 | var instanceMock = {}; 19 | var instanceMock2 = {}; 20 | hotRegisterer.registerInstance('foo', instanceMock); 21 | hotRegisterer.registerInstance('foo', instanceMock2); 22 | 23 | expect(hotRegisterer.getInstance('foo')).toBe(instanceMock2); 24 | })); 25 | 26 | it('should remove Handsontable instance', inject(function(hotRegisterer) { 27 | var instanceMock = {}; 28 | hotRegisterer.registerInstance('foo', instanceMock); 29 | hotRegisterer.removeInstance('foo'); 30 | 31 | expect(hotRegisterer.getInstance('foo')).not.toBeDefined(); 32 | })); 33 | }); 34 | -------------------------------------------------------------------------------- /test/services/settingFactory.spec.js: -------------------------------------------------------------------------------- 1 | 2 | describe('settingFactory', function() { 3 | 4 | beforeEach(module('ngHandsontable.services')); 5 | 6 | it('should have defined property container className', inject(function(settingFactory) { 7 | expect(settingFactory.containerClassName).toBe('handsontable-container'); 8 | })); 9 | 10 | it('should create new div element and instance Handsontable injected to it (initializeHandsontable)', inject(function(settingFactory) { 11 | var element = [{appendChild: jasmine.createSpy('appendChild')}]; 12 | var hotSettings = {colHeaders: [1, 2], width: 200, columns: [{width: 100}]}; 13 | var hotInstance = {}; 14 | 15 | window.HandsontableOrg = Handsontable; 16 | window.Handsontable = function(element, settings) { 17 | hotInstance.element = element; 18 | hotInstance.settings = settings; 19 | }; 20 | 21 | settingFactory.initializeHandsontable(element, hotSettings); 22 | 23 | expect(element[0].appendChild).toHaveBeenCalled(); 24 | expect(element[0].appendChild.calls.argsFor(0)[0].nodeName).toBe('DIV'); 25 | expect(hotInstance.element.nodeName).toBe('DIV'); 26 | expect(hotInstance.settings).toBe(hotSettings); 27 | 28 | window.Handsontable = HandsontableOrg; 29 | })); 30 | 31 | it('should set "id" attribute of wrapper element when "hot-id" attribute is defined', inject(function(settingFactory) { 32 | var element = [{appendChild: jasmine.createSpy('appendChild')}]; 33 | var hotSettings = {hotId: 'my-table', colHeaders: [1, 2], width: 200, columns: [{width: 100}]}; 34 | var hotInstance = {}; 35 | 36 | window.HandsontableOrg = Handsontable; 37 | window.Handsontable = function(element, settings) { 38 | hotInstance.element = element; 39 | hotInstance.settings = settings; 40 | }; 41 | 42 | settingFactory.initializeHandsontable(element, hotSettings); 43 | 44 | expect(element[0].appendChild.calls.argsFor(0)[0].id).toBe('my-table'); 45 | 46 | window.Handsontable = HandsontableOrg; 47 | })); 48 | 49 | it('should update Handsontable settings (updateHandsontableSettings)', inject(function(settingFactory) { 50 | var hotInstance = {updateSettings: jasmine.createSpy('updateSettings')}; 51 | var hotSettings = {colHeaders: [1, 2], width: 200, columns: [{width: 100}]}; 52 | 53 | settingFactory.updateHandsontableSettings(null, hotSettings); 54 | 55 | expect(hotInstance.updateSettings).not.toHaveBeenCalled(); 56 | 57 | settingFactory.updateHandsontableSettings(hotInstance, hotSettings); 58 | 59 | expect(hotInstance.updateSettings).toHaveBeenCalledWith(hotSettings); 60 | })); 61 | 62 | it('should render Handsontable (renderHandsontable)', inject(function(settingFactory) { 63 | var hotInstance = {render: jasmine.createSpy('render')}; 64 | 65 | settingFactory.renderHandsontable(null); 66 | 67 | expect(hotInstance.render).not.toHaveBeenCalled(); 68 | 69 | settingFactory.renderHandsontable(hotInstance); 70 | 71 | expect(hotInstance.render).toHaveBeenCalled(); 72 | })); 73 | 74 | it('should merge Handsontable settings from scope (mergeSettingsFromScope)', inject(function(settingFactory) { 75 | var scopeMock = { 76 | colHeaders: [1], 77 | manualColumnResize: true, 78 | settings: { 79 | data: [{id: 1}, {id: 2}] 80 | }, 81 | manualColumnMove: void 0 82 | }; 83 | var settings = { 84 | manualColumnMove: true 85 | }; 86 | 87 | settingFactory.mergeSettingsFromScope(settings, scopeMock); 88 | 89 | expect(settings.colHeaders).toEqual([1]); 90 | expect(settings.data).toEqual([{id: 1}, {id: 2}]); 91 | expect(settings.manualColumnResize).toBe(true); 92 | expect(settings.manualColumnMove).toBe(true); 93 | })); 94 | 95 | it('should merge Handsontable hooks from scope (mergeHooksFromScope)', inject(function(settingFactory) { 96 | var fnAfterInit = function() {}; 97 | var fnBeforeInit = function() {}; 98 | var fnAfterChange = function() {}; 99 | var scopeMock = { 100 | afterInit: fnAfterInit, 101 | settings: { 102 | onBeforeInit: fnBeforeInit 103 | }, 104 | onAfterChange: void 0 105 | }; 106 | var settings = { 107 | afterChange: fnAfterChange 108 | }; 109 | 110 | settingFactory.mergeHooksFromScope(settings, scopeMock); 111 | 112 | expect(settings.afterInit).toBe(fnAfterInit); 113 | expect(settings.beforeInit).toBe(fnBeforeInit); 114 | expect(settings.afterChange).toBe(fnAfterChange); 115 | })); 116 | 117 | it('should trim scope definition according to attrs object (trimScopeDefinitionAccordingToAttrs)', inject(function(settingFactory) { 118 | var scopeDefinition = { 119 | colHeaders: '=', 120 | manualColumnResize: '=', 121 | columnSorting: '=', 122 | afterInit: '&onAfterInit' 123 | }; 124 | var attrsMock = { 125 | colHeaders: 'col-headers', 126 | rowHeaders: 'row-headers', 127 | onAfterInit: '' 128 | }; 129 | 130 | settingFactory.trimScopeDefinitionAccordingToAttrs(scopeDefinition, attrsMock); 131 | 132 | expect(scopeDefinition.colHeaders).toBe('='); 133 | expect(scopeDefinition.afterInit).toBe('&onAfterInit'); 134 | expect(scopeDefinition.manualColumnResize).not.toBeDefined(); 135 | expect(scopeDefinition.columnSorting).not.toBeDefined(); 136 | })); 137 | 138 | it('should return scope definition for hot-table directive (getTableScopeDefinition)', inject(function(settingFactory) { 139 | spyOn(settingFactory, 'applyAvailableSettingsScopeDef'); 140 | spyOn(settingFactory, 'applyAvailableHooksScopeDef'); 141 | 142 | var settings = settingFactory.getTableScopeDefinition(); 143 | 144 | expect(settingFactory.applyAvailableSettingsScopeDef).toHaveBeenCalled(); 145 | expect(settingFactory.applyAvailableHooksScopeDef).toHaveBeenCalled(); 146 | expect(settings.datarows).toBe('='); 147 | expect(settings.dataschema).toBe('='); 148 | expect(settings.observeDomVisibility).toBe('='); 149 | expect(settings.settings).toBeUndefined(); 150 | })); 151 | 152 | it('should return scope definition for hot-column directive (getColumnScopeDefinition)', inject(function(settingFactory) { 153 | spyOn(settingFactory, 'applyAvailableSettingsScopeDef'); 154 | spyOn(settingFactory, 'applyAvailableHooksScopeDef'); 155 | 156 | var settings = settingFactory.getColumnScopeDefinition(); 157 | 158 | expect(settingFactory.applyAvailableSettingsScopeDef).toHaveBeenCalled(); 159 | expect(settingFactory.applyAvailableHooksScopeDef).not.toHaveBeenCalled(); 160 | expect(settings.data).toBe('@'); 161 | })); 162 | 163 | it('should apply all available settings into provided scope definition object (applyAvailableSettingsScopeDef)', inject(function(settingFactory) { 164 | spyOn(settingFactory, 'getAvailableSettings'); 165 | spyOn(settingFactory, 'getAvailableHooks'); 166 | 167 | settingFactory.getAvailableSettings.and.returnValue(['colHeaders', 'rowHeaders']); 168 | settingFactory.getAvailableHooks.and.returnValue(['afterInit', 'beforeInit']); 169 | 170 | var scopeDef = {}; 171 | var settings = settingFactory.applyAvailableSettingsScopeDef(scopeDef); 172 | 173 | expect(settingFactory.getAvailableSettings).toHaveBeenCalled(); 174 | expect(settingFactory.getAvailableHooks).not.toHaveBeenCalled(); 175 | expect(settings.colHeaders).toBe('='); 176 | // not exist in available settings collection 177 | expect(settings.columnSorting).not.toBeDefined(); 178 | // hooks 179 | expect(settings.afterInit).not.toBeDefined(); 180 | expect(settings.beforeInit).not.toBeDefined(); 181 | })); 182 | 183 | it('should apply all available hooks into provided scope definition object (applyAvailableHooksScopeDef)', inject(function(settingFactory) { 184 | spyOn(settingFactory, 'getAvailableSettings'); 185 | spyOn(settingFactory, 'getAvailableHooks'); 186 | 187 | settingFactory.getAvailableSettings.and.returnValue(['colHeaders', 'rowHeaders']); 188 | settingFactory.getAvailableHooks.and.returnValue(['afterInit', 'beforeInit']); 189 | 190 | var scopeDef = {}; 191 | var settings = settingFactory.applyAvailableHooksScopeDef(scopeDef); 192 | 193 | expect(settingFactory.getAvailableSettings).not.toHaveBeenCalled(); 194 | expect(settingFactory.getAvailableHooks).toHaveBeenCalled(); 195 | // settings 196 | expect(settings.colHeaders).not.toBeDefined(); 197 | expect(settings.columnSorting).not.toBeDefined(); 198 | // hooks 199 | expect(settings.afterInit).toBe('=onAfterInit'); 200 | expect(settings.beforeInit).toBe('=onBeforeInit'); 201 | // not exist in available hooks collection 202 | expect(settings.afterChange).not.toBeDefined(); 203 | })); 204 | 205 | it('should return all available settings in camelCase mode', inject(function(settingFactory) { 206 | var settings = settingFactory.getAvailableSettings(); 207 | 208 | expect(settings.length).toBeGreaterThan(0); 209 | expect(settings.indexOf('colHeaders')).toBeGreaterThan(-1); 210 | expect(settings.indexOf('minSpareRows')).toBeGreaterThan(-1); 211 | expect(settings.indexOf('data')).toBeGreaterThan(-1); 212 | expect(settings.indexOf('afterInit')).toBe(-1); 213 | })); 214 | 215 | it('should return all available settings in hyphenate mode', inject(function(settingFactory) { 216 | var settings = settingFactory.getAvailableSettings(true); 217 | 218 | expect(settings.length).toBeGreaterThan(0); 219 | expect(settings.indexOf('col-headers')).toBeGreaterThan(-1); 220 | expect(settings.indexOf('min-spare-rows')).toBeGreaterThan(-1); 221 | expect(settings.indexOf('data')).toBeGreaterThan(-1); 222 | expect(settings.indexOf('afterInit')).toBe(-1); 223 | })); 224 | 225 | it('should return all hooks in camelCase mode', inject(function(settingFactory) { 226 | var settings = settingFactory.getAvailableHooks(); 227 | 228 | expect(settings.length).toBeGreaterThan(0); 229 | expect(settings.indexOf('afterInit')).toBeGreaterThan(-1); 230 | expect(settings.indexOf('beforeInit')).toBeGreaterThan(-1); 231 | expect(settings.indexOf('construct')).toBeGreaterThan(-1); 232 | expect(settings.indexOf('colHeaders')).toBe(-1); 233 | })); 234 | 235 | it('should return all hooks settings in hyphenate mode', inject(function(settingFactory) { 236 | var settings = settingFactory.getAvailableHooks(true); 237 | 238 | expect(settings.length).toBeGreaterThan(0); 239 | expect(settings.indexOf('on-after-init')).toBeGreaterThan(-1); 240 | expect(settings.indexOf('on-before-init')).toBeGreaterThan(-1); 241 | expect(settings.indexOf('on-construct')).toBeGreaterThan(-1); 242 | expect(settings.indexOf('on-col-headers')).toBe(-1); 243 | })); 244 | }); 245 | --------------------------------------------------------------------------------