diff --git a/.gitignore b/.gitignore index aa461ff9..3e123f5a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea node_modules -bower_components \ No newline at end of file +bower_components +bower_components/handsontable \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index d2ba41df..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,190 +0,0 @@ -## 0.3.17 (Oct 1, 2013) - -Upgrade Handsontable to 0.9.19 (see [changelog](https://github.com/warpech/jquery-handsontable/blob/master/CHANGELOG.md)) - -Includes: -- improved native vertical scrollbar (use directive `scrollbarModelV="'native'"`) -- new plugin hook `afterRenderer` - -## 0.3.16 (Aug 22, 2013) - -Upgrade Handsontable to 0.9.14 (see [changelog](https://github.com/warpech/jquery-handsontable/blob/master/CHANGELOG.md)) - -## 0.3.15 (Jul 31, 2013) - -Fix a bug introduced in 0.3.14 (some of the options were not correct). - -## 0.3.14 (Jul 31, 2013) - -Allow all Handsontable options introduced in few recent versions, e.g. `fragmentSelection` (see [Options](https://github.com/warpech/jquery-handsontable/wiki/Options)) - -## 0.3.13 (Jul 29, 2013) - -Other: -- upgrade Handsontable to 0.9.11 (see [changelog](https://github.com/warpech/jquery-handsontable/blob/master/CHANGELOG.md)) - -## 0.3.12 (Jul 23, 2013) - -Other: -- upgrade Handsontable to 0.9.10 (see [changelog](https://github.com/warpech/jquery-handsontable/blob/master/CHANGELOG.md)) - -## 0.3.11 (Jun 17, 2013) - -Other: -- upgrade Handsontable to 0.9.5 - -## 0.3.10 (Jun 12, 2013) - -Other: -- define Handsontable as Bower dependency -- upgrade Handsontable to 0.9.4 - -## 0.3.9 (May 15, 2013) - -Bugfix: -- fix "Cannot read property 'offsetLeft' of undefined at WalkontableDom.offset" issue when using UI Bootstrap - -Other: -- upgrade Handsontable to 0.9-beta2 - -## 0.3.8 (May 12, 2013) - -Bugfix: -- fix "$digest already in progress" issue when using UI Bootstrap -- fix "firstChild not found" issue when using UI Bootstrap - -Other: -- upgrade Handsontable to 0.9-beta1 (dev branch) - -## 0.3.7 (May 3, 2013) - -Bugfix: -- `ui-handsontable` did not work when placed inside [UI Bootstrap](http://angular-ui.github.io/bootstrap/) `` directive -- added tabs.html demo that uses UI Bootstrap - -Other: -- upgrade Handsontable to 0.8.23 - -## 0.3.6 (Mar 26, 2013) - -Feature: -- split-screen.html demo now uses all available space in window - -Other: -- upgrade Handsontable to 0.8.16 - -## 0.3.5 (Mar 24, 2013) - -Bugfix: -- `datarows` crashed when trying to use deep object structure as the data source (`datarows="row in sql.Rows"`) - -## 0.3.4 (Mar 4, 2013) - -Bugfix: -- fix problems with autocomplete cell type and Angular Patch integration - -Other: -- upgrade Handsontable to 0.8.8 -- upgrade AngularJS to 1.0.5 - -## 0.3.3 (Feb 28, 2013) - -Features: -- new numeric cell type -- make autocomplete selection much faster - -Other: -- upgrade Handsontable to 0.8.6 -- upgrade jQuery to 1.9.1 -- upgrade build system to Grunt 0.4.0 (read instructions how to upgrade here: http://gruntjs.com/upgrading-from-0.3-to-0.4) - -## 0.3.2 (Jan 23, 2013) - -Bugfixes: -- upgrade Handsontable to 0.8.3 -- fix "Non-assignable model expression" error when `selectedIndex` attribute was a primitive number, not a object property reference -- when starting with 0 rows, then adding a new row, table was not rerendered -- column stretching did not work with 0 rows -- horizontal scrollbar was shown with 0 rows - -## 0.3.1 (Jan 21, 2013) - -Features: -- new syntax supported. Handsontable attributes should now be passed at directive attributes (eg. `minSpareRows="1"`) -- in addition to the above, the attributes may be dynamic variables that will be observed for changes (eg. `columns="myColumns"`) -- new attribute `selectedIndex` allows to bind a scope variable to get/set selected row index - -Bugfixes: -- `onChange` callback is called before any other events when clicked outside of the grid -- column stretching does not flicker when scrolling in IE, FF, Opera -- clicking outside of table finishes editing of the cell - -## 0.3.0 (Jan 14, 2013) - -Features: -- highlight currently highlighted row -- manual column resize -- column autosize when double clicked on the manual column resize handle -- column stretching -- column sorting -* table now automatically fits the container when window is resized - -Known issues: -- last column flickers when scrolling in IE, FF, Opera -- first example on split-screen.html uses whole screen witdh if "width" parameter is not provided (width: 640) - -## 0.2.0 (Jan 7, 2013) - -- virtual rendering fixes and optimizations (upgrade to Handsontable 0.8.2) -- show only relevant scrollbars -- dynamic columns defined in split-screen.js - -## 0.2-beta3 (Dec 19, 2012) - -- virtual rendering fixes and optimizations -- watch for changes only in visible part of the table - -## 0.2-beta2 (Dec 13, 2012) - -- numerous virtual rendering fixes and optimizations -- defining column widths using the `width` attribute - -## 0.2-beta1 (Dec 6, 2012) - -- virtual rendering allowing for big number of editable rows - -## 0.1.5 (Nov 22, 2012) - -- fix for removing rows in data source - -## 0.1.4 (Nov 19, 2012) - -- cell border is now always rerendered after editing -- upgrade Handsontable to 0.7.5-dev - -## 0.1.3 (Nov 16, 2012) - -- changed module name to `uiHandsontable` to avoid conflict with Angular UI -- created demo page `ui.html` to test cooperability with Angular UI - -## 0.1.2 (Nov 16, 2012) - -- now propagates changes correctly if data source comes from parent scope (changed scope.$digest to scope.$apply) -- IE 8 shim now is included in `full` and `full.min` JavaScript packages -- upgrade Handsontable to 0.7.4-dev -- removed `live` attribute from autocomplete (it is now always assumed). Introduced `saveOnBlur` attribute which has the opposite behavior than `live` - -## 0.1.1 (Nov 13, 2012) - -- now the template inside `optionlist` is compiled along with inner Angular directives -- now you can use the uiHandsontable directive as attribute `
` or element `` (both are used in split-screen.html) -- split-screen.html now uses `img ng-src` instead of `img src` so that there is no request to the server for the unparsed "{{option.Image}}" path - -## 0.1.0 (Nov 12, 2012) - -Changes since Nov 5, 2012: - -- now Angular UI Handsontable is built with Grunt -- 2 distributions (in [dist/](https://github.com/warpech/angular-ui-handsontable/tree/master/dist) directory): full and full.min (for development purposes I think it is better to use full for now) -- directive name changed to `ui-handsontable` to follow the scheme that Angular UI is using -- new directive `optionlist` that includes the autocomplete row template inside \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js index 6b0e3615..16c9f99b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,5 +1,5 @@ /** - * This file is used to build Angular UI Handsontable from `src/*` + * This file is used to build ngHandsontable from `src/*` * * Installation: * 1. Install Grunt CLI (`npm install -g grunt-cli`) @@ -10,24 +10,26 @@ * To execute automatically after each change, execute `grunt --force default watch` * * Result: - * building Angular UI Handsontable will create files: - * - dist/angular-ui-handsontable.full.js - * - dist/angular-ui-handsontable.full.css - * - dist/angular-ui-handsontable.full.min.js - * - dist/angular-ui-handsontable.full.min.css + * building ngHandsontable will create files: + * - dist/ngHandsontable.js + * - dist/ngHandsontable.min.js * * See http://gruntjs.com/getting-started for more information about Grunt */ module.exports = function (grunt) { var myBanner = '/**\n' + ' * <%= pkg.name %> <%= pkg.version %>\n' + + ' * \n' + + ' * Copyright 2012-2014 Marcin Warpechowski\n' + + ' * Licensed under the MIT license.\n' + + ' * https://github.com/handsontable/ngHandsontable\n' + ' * Date: <%= (new Date()).toString() %>\n' + '*/\n\n'; - grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), - concat: { + + concat: { options: { banner: myBanner }, @@ -39,47 +41,28 @@ module.exports = function (grunt) { 'src/directives/*.js' ], dest: 'dist/ngHandsontable.js' - }, - full_js: { - src: [ - 'src/ie-shim.js', - 'src/angular-ui-handsontable.js', - 'bower_components/handsontable/dist/jquery.handsontable.full.js' - ], - dest: 'dist/angular-ui-handsontable.full.js' - }, - full_css: { - src: [ - 'bower_components/handsontable/dist/jquery.handsontable.full.css' - ], - dest: 'dist/angular-ui-handsontable.full.css' - } + } }, - uglify: { + + uglify: { options: { banner: myBanner }, - "dist/angular-ui-handsontable.full.min.js": ["dist/angular-ui-handsontable.full.js" ], "dist/ngHandsontable.min.js": ["dist/ngHandsontable.js"] }, - cssmin: { - options: { - banner: myBanner - }, - "dist/angular-ui-handsontable.full.min.css": ["dist/angular-ui-handsontable.full.css" ] - }, - watch: { + + watch: { files: ['src/**/*', 'bower_components/**/*'], - tasks: ['concat', 'uglify', 'cssmin'] + tasks: ['concat', 'uglify'] } }); // Default task. - grunt.registerTask('default', ['concat', 'uglify', 'cssmin']); +// grunt.registerTask('default', ['concat', 'uglify', 'cssmin']); + + grunt.registerTask('default', ['concat', 'uglify']); - grunt.loadNpmTasks('grunt-css'); grunt.loadNpmTasks('grunt-contrib-uglify'); - grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-watch'); }; \ No newline at end of file diff --git a/LICENSE b/LICENSE index b6a44c7c..fb728388 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ (The MIT License) -Copyright (c) 2012 Marcin Warpechowski +Copyright (c) 2012 Marcin Warpechowski Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md index a1498fff..be879fc1 100644 --- a/README.md +++ b/README.md @@ -1,92 +1,98 @@ -# Angular UI directive for Handsontable +# ngHandsontable - the AngularJS directive for [Handsontable](https://github.com/handsontable/jquery-handsontable) Enables creation of data grid applications in AngularJS. ## Demo -The current version should be deployed here: http://ng-datagrid.handsontable.com/split-screen.html - -You can also clone this repo and run `split-screen.html` in your browser +See the demo at http://handsontable.github.io/ngHandsontable ## Usage -Include the library files (see [dist/](https://github.com/warpech/angular-ui-handsontable/tree/master/dist) directory): +Include the library files: ```html - - - - + + + + + ``` Template: ```html -
- - - - - - - - {{option.Description}} - - - -
+ + + + + + + + + + + ``` Controller: ```javascript -$scope.items = [ +$scope.db.items = [ { - id: 1, - name: { - first: "Marcin", - last: "Warpechowski" + "id":1, + "name":{ + "first":"John", + "last":"Schmidt" }, - address: "Marienplatz 11, Munich", - isActive: "Yes", - Product: { - Description: "Big Mac", - Options: [ - {Description: "Big Mac"}, - {Description: "Big Mac & Co"} - ] - } - } + "address":"45024 France", + "price":760.41, + "isActive":"Yes", + "product":{ + "description":"Fried Potatoes", + "options":[ + { + "description":"Fried Potatoes", + "image":"//a248.e.akamai.net/assets.github.com/images/icons/emoji/fries.png", + "Pick$":null + }, + { + "description":"Fried Onions", + "image":"//a248.e.akamai.net/assets.github.com/images/icons/emoji/fries.png", + "Pick$":null + } + ] + } + }, //more items go here ]; ``` - -Please note that in the above example, the `item.Product.Description` column has autocomplete options returned by a function defined in the controller. - -Whereas `item.isActive` column has autocomplete options defined directly in the parental scope. ## Directives and attributes specification -All **Handsontable** attributes listed [here](https://github.com/warpech/jquery-handsontable) should be supported (namely: width, height, rowHeaders, colHeaders, colWidths, columns, cells, dataSchema, contextMenu, onSelection, onSelectionByProp, onBeforeChange, onChange, onCopyLimit, startRows, startCols, minRows, minCols, maxRows, maxCols, minSpareRows, minSpareCols, multiSelect, fillHandle, undo, outsideClickDeselects, enterBeginsEditing, enterMoves, tabMoves, autoWrapRow, autoWrapCol, copyRowsLimit, copyColsLimit, currentRowClassName, currentColClassName, asyncRendering, stretchH, columnSorting) +All **Handsontable** options listed [here](https://github.com/handsontable/jquery-handsontable/wiki) should be supported Directive | Attribute    | Description --------------------------------|-----------------------------|------------- - **<div ui-handsontable>** | | Defines the grid container. Can also be declared as element `` - <div ui-handsontable> | datarows | Data provider for the grid. Usage like `item in items` (similar to ngRepeat). Creates new scope for each row - <div ui-handsontable> | settings | jquery-handsontable settings. For list of options, see: [warpech/jquery-handsontable](https://github.com/warpech/jquery-handsontable) - <div ui-handsontable> | selectedIndex | Allows to bind a scope variable to get/set selected row index - **<datacolumn>** | | Defines a column in the grid - <datacolumn> | type | Column type. Possible values: `text`, `checkbox`, `autocomplete` (default: `text`) - <datacolumn> | value | Row property that will be used as data source for each cell - <datacolumn> | title | Column title - <datacolumn> | readOnly | If set, column will be read-only - <datacolumn> | saveOnBlur | (Autocomplete columns only) If set, `value` will be updated after autocomplete is blured. This is in contrast to default behavior, where `value` is updated after each keystroke - <datacolumn> | strict | (Autocomplete columns only) If set, `value` can only be selected from autocomplete options. If not set, also custom `value` is allowed if entered to the text box - <datacolumn> | checkedTemplate | (Checkbox columns only) Expression that will be used as the value for checked `checkbox` cell (default: boolean `true`) - <datacolumn> | uncheckedTemplate | (Checkbox columns only) Expression that will be used as the value for unchecked `checkbox` cell (default: boolean `false`) - -## Further development - -This is not considered production ready. When the work is finished, contents of this repo will be submitted into https://github.com/warpech/angular-ui/ \ No newline at end of file + **<div hot-table>** | | Defines the grid container. Can also be declared as element `` + <div hot-table> | datarows | Data provider for the grid. Usage like `item in items` (similar to ngRepeat). Creates new scope for each row + <div hot-table> | settings | jquery-handsontable settings. For list of options, see: [handsontable/jquery-handsontable](https://github.com/handsontable/jquery-handsontable) + <div hot-table> | selectedIndex | Allows to bind a scope variable to get/set selected row index + **<hot-column>** | | Defines a column in the grid + <hot-column> | type | Column type. Possible values: `text`, `checkbox`, `autocomplete` (default: `text`) + <hot-column> | value | Row property that will be used as data source for each cell + <hot-column> | title | Column title + <hot-column> | readOnly | If set, column will be read-only + <hot-column> | saveOnBlur | (Autocomplete columns only) If set, `value` will be updated after autocomplete is blured. This is in contrast to default behavior, where `value` is updated after each keystroke + <hot-column> | strict | (Autocomplete columns only) If set, `value` can only be selected from autocomplete options. If not set, also custom `value` is allowed if entered to the text box + <hot-column> | checkedTemplate | (Checkbox columns only) Expression that will be used as the value for checked `checkbox` cell (default: boolean `true`) + <hot-column> | uncheckedTemplate | (Checkbox columns only) Expression that will be used as the value for unchecked `checkbox` cell (default: boolean `false`) + +## License + +The MIT License (see the [LICENSE](https://github.com/handsontable/ngHandsontable/blob/master/LICENSE) file for the full text) \ No newline at end of file diff --git a/bower.json b/bower.json index 9f269ca5..2653c1c7 100644 --- a/bower.json +++ b/bower.json @@ -1,12 +1,32 @@ { - "name": "ng-datagrid", - "version": "0.3.17", - "devDependencies": { - "handsontable": "" - }, + "name": "ngHandsontable", + "version": "0.5.0", "dependencies": { "angular": "~1.2.25", - "jquery": "~2.0.3", - "angular-ui-select2": "~0.0.2" - } -} \ No newline at end of file + "handsontable": "~0.12" + }, + "homepage": "https://github.com/handsontable/ngHandsontable", + "authors": [ + "Marcin Warpechowski " + ], + "description": "AngularJS directive for Handsontable", + "main": "dist/ngHandsontable.js", + "keywords": [ + "angular", + "angularjs", + "handsontable", + "datagrid", + "grid", + "table" + ], + "license": "MIT", + "ignore": [ + "**/.*", + "bower_components", + "demo", + "node_modules", + "src", + "test", + "tests" + ] +} diff --git a/bower_components/angular-ui-select2/.bower.json b/bower_components/angular-ui-select2/.bower.json deleted file mode 100644 index 514103d4..00000000 --- a/bower_components/angular-ui-select2/.bower.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "author": "AngularUI", - "name": "angular-ui-select2", - "version": "0.0.2", - "homepage": "http://angular-ui.github.com", - "keywords": [ - "angular", - "angularui", - "select2" - ], - "main": "./src/select2.js", - "dependencies": { - "angular": ">= 1.0.2", - "select2": "~3.4", - "jquery": ">=1.6.4" - }, - "devDependencies": { - "angular-mocks": ">= 1.0.2" - }, - "_release": "0.0.2", - "_resolution": { - "type": "version", - "tag": "v0.0.2", - "commit": "117bbbc0408a5933f580c6fe7b8e977181d01199" - }, - "_source": "git://github.com/angular-ui/ui-select2.git", - "_target": "~0.0.2", - "_originalSource": "angular-ui-select2" -} \ No newline at end of file diff --git a/bower_components/angular-ui-select2/.gitignore b/bower_components/angular-ui-select2/.gitignore deleted file mode 100644 index 097a86f7..00000000 --- a/bower_components/angular-ui-select2/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -components \ No newline at end of file diff --git a/bower_components/angular-ui-select2/.travis.yml b/bower_components/angular-ui-select2/.travis.yml deleted file mode 100644 index d86175cb..00000000 --- a/bower_components/angular-ui-select2/.travis.yml +++ /dev/null @@ -1,11 +0,0 @@ - language: node_js - node_js: - - "0.8" - - before_install: - - export DISPLAY=:99.0 - - sh -e /etc/init.d/xvfb start - - npm install -g testacular@0.4.x bower - - bower install - - script: "testacular start test/test.conf.js --browsers=Firefox,PhantomJS" diff --git a/bower_components/angular-ui-select2/CHANGELOG.md b/bower_components/angular-ui-select2/CHANGELOG.md deleted file mode 100644 index 038ca1c6..00000000 --- a/bower_components/angular-ui-select2/CHANGELOG.md +++ /dev/null @@ -1,60 +0,0 @@ -# [Cha Cha Cha Changes](http://www.youtube.com/watch?v=pl3vxEudif8&t=0m53s) - -## Master - -## v0.4.0 -* **Validate directive** has been upgraded - * **API BREAKING CHANGE!** now takes expressions instead of function references - * You must explicitly specify the $value variable, but you no longer need to create a function - * **NEW FEATURE** uiValidateWatch allows you to re-fire a validation rule (or all rules) when a related model changes (confirm_password) -* **CodeMirror directive** has been updated - * Now works with v3.02 - * **NEW FEATURE** uiRefresh lets you specify an expression to watch for changes to refresh codemirror (useful for modals) -* **Mask directive** has many new fixes -* Fixes for **uiDate** - * **DateFormat directive** can now be declared in **uiConfig** -* **uiJq Passthru directive** has upgrades to support a wider variety of directives - * Now fires asyncronously post-angular-rendering of the view (**uiDefer** option is now always true) - * New **uiRefresh** lets you specify an expression to watch to re-fire the plugin (call $(elm).focus() when a modal opens) -* **Select2 directive** now adds support for setting the selected item by specifying a simple ID - * FINALLY have unit-tests for Select2! -* **IEShiv** has been simplified and stripped of browser-sniffing code (just use conditional comments) -* **Calendar directive** now performs better watching of events data - * Added optional equalsTracker attr (increment to force update from scope) -* **Sortable directive** now properly supports connectWith option -* New **route directive** that sets a boolean based on a pattern match of the current route (useful for tabs/navigation) -* Refactored **If directive** to be tidier -* **API BREAKING CHANGE!** **Modal directive** has been completely removed (if you still need it, grab the files from v0.3.x) - -## v0.3.0 -* New **format** filter -* Lots of cleanup! Consistent indentation, linting -* Custom builds via grunt (soon to be leveraged via builder) -* uiDate now watches options -* Rewrote ui-keypress (API is not backwards-compatible) - * **ui-**keypress has been expanded into **ui-keyup**, **ui-keydown** and **ui-keypress** - * The **ui-keypress** can now be used to `$event.preventDefault()` as expected - * Multiple combinations are separated by spaces, while multi-key combos are separated by dashes: `'enter alt-space 13-shift':'whatever()'` - * The string-notation (__a and be or c and d__) has been dropped completely -* Can now pass (or globally define) the value uiReset resets to - -## v0.2.0 -* Unit tests. Unit tests. Unit tests. -* New **inflector** filter (previously named **prettifier**) - * Added 2 alternative modes, now contains: humanize, underscore and variable -* **Passthrough directive** (uiJq) now fixes common ngModel problems due to trigger(change). Can optionally be disabled -* Removed **Length Filter** (you can instead do {{ ( myArray | filter: { gender:'m' } ).length }}) -* Added **validate directive**, allows you to pass validation functions -* **Sortable directive** -* Fixed **unique filter** -* **Highlight filter** has had bug fixes -* **Event directive** has been refactored / improved -* **Keypress directive** has been refactored / improved -* New **if-directive** instead of **remove directive** (removed) -* New **google maps directive** -* New **animate directive** that transitions the injection of new DOM elements (transitioning the removal of DOM is still not supported yet) -* Improvements to **scrollfix directive** - -## v0.1.0 -* New folder structure -* Too many to list diff --git a/bower_components/angular-ui-select2/CONTRIBUTING.md b/bower_components/angular-ui-select2/CONTRIBUTING.md deleted file mode 100644 index cb66f02b..00000000 --- a/bower_components/angular-ui-select2/CONTRIBUTING.md +++ /dev/null @@ -1,8 +0,0 @@ -CONTRIBUTING -============ - -* Open a [Pull Request (PR)](https://github.com/angular-ui/ui-select2/pull/new/master) -* Make sure your PR is on a **new branch** you created off of the latest version of master -* Do **not** open a PR from your master branch -* Open a PR to start a discussion even if the code isn't finished (easier to collect feedback this way) -* Make sure all previous tests pass and add new tests for added behaviors diff --git a/bower_components/angular-ui-select2/LICENSE b/bower_components/angular-ui-select2/LICENSE deleted file mode 100644 index dfc5e0ca..00000000 --- a/bower_components/angular-ui-select2/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License - -Copyright (c) 2012 the AngularUI Team, http://angular-ui.github.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/bower_components/angular-ui-select2/README.md b/bower_components/angular-ui-select2/README.md deleted file mode 100644 index 93382381..00000000 --- a/bower_components/angular-ui-select2/README.md +++ /dev/null @@ -1,133 +0,0 @@ -ui-select2 [![Build Status](https://travis-ci.org/angular-ui/ui-select2.png)](https://travis-ci.org/angular-ui/ui-select2) -========== -This directive allows you to enhance your select elements with behaviour from the [select2](http://ivaynberg.github.io/select2/) library. - -# Requirements - -- [AngularJS](http://angularjs.org/) -- [JQuery](http://jquery.com/) -- [Select2](http://ivaynberg.github.io/select2/) - -## Setup - -1. Install **karma** - `$ npm install -g karma` -2. Install **bower** - `$ npm install -g bower` -4. Install components - `$ bower install` -4. ??? -5. Profit! - -## Testing - -`$ karma start test/test.conf.js --browsers=Chrome` - -# Usage - -We use [bower](http://twitter.github.com/bower/) for dependency management. Add - -```javascript -dependencies: { - "angular-ui-select2": "latest" -} -``` - -To your `components.json` file. Then run - - bower install - -This will copy the ui-select2 files into your `components` folder, along with its dependencies. Load the script files in your application: -```html - - - -``` - -Add the select2 module as a dependency to your application module: - -```javascript -var myAppModule = angular.module('MyApp', ['ui.select2']); -``` - -Apply the directive to your form elements: - -```html - -``` - -## Options - -All the select2 options can be passed through the directive. You can read more about the supported list of options and what they do on the [Select2 Documentation Page](http://ivaynberg.github.com/select2/) - -```javascript -myAppModule.controller('MyController', function($scope) { - $scope.select2Options = { - allowClear:true - }; -}); -``` - -```html - -``` - -Some time it may make sense to specify the options in the template file. - -```html - -``` - -## Working with ng-model - -The ui-select2 directive plays nicely with ng-model and validation directives such as ng-required. - -If you add the ng-model directive to same the element as ui-select2 then the picked option is automatically synchronized with the model value. - -## Working with dynamic options -`ui-select2` is incompatible with ` - - - -``` - -## Working with placeholder text -In order to properly support the Select2 placeholder, create an empty ` - - - - -``` - -## ng-required directive - -If you apply the required directive to element then the form element is invalid until an option is selected. - -Note: Remember that the ng-required directive must be explicitly set, i.e. to "true". This is especially true on divs: - -```html - -``` diff --git a/bower_components/angular-ui-select2/bower.json b/bower_components/angular-ui-select2/bower.json deleted file mode 100644 index 3c3b3eb7..00000000 --- a/bower_components/angular-ui-select2/bower.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "author": "AngularUI", - "name": "angular-ui-select2", - "version": "0.0.2", - "homepage": "http://angular-ui.github.com", - "keywords": [ - "angular", - "angularui", - "select2" - ], - "main": "./src/select2.js", - "dependencies": { - "angular": ">= 1.0.2", - "select2": "~3.4", - "jquery": ">=1.6.4" - }, - "devDependencies": { - "angular-mocks": ">= 1.0.2" - } -} diff --git a/bower_components/angular-ui-select2/docs/index.html b/bower_components/angular-ui-select2/docs/index.html deleted file mode 100644 index cae3c001..00000000 --- a/bower_components/angular-ui-select2/docs/index.html +++ /dev/null @@ -1,38 +0,0 @@ -
- -
-
-

Demo

-
-

Value is: {{select2}} (choose second)

- -
-
-
-

Options

-

You can pass an object to Select2 as the expression: ui-select2="{allowClear:true}" that will be passed directly to $.fn.select2(). You can read more about the supported list of options and what they do on the Select2 Documentation Page. AngularUI will leverage properties passed to Select2 for any complex behavior, there are no parameters necessary for that are specific to AngularUI.

-
-
-

ui-select2 is incompatible with <select ng-options>. For the best results use <option ng-repeat> instead

-

In order to properly support the Select2 placeholder, create an empty <option> tag at the top of the <select> and either set a data-placeholder on the select element or pass a placeholder option to Select2.

- -

How?

-
-<p>Value is: {{select2}} <a ng-click="select2='two'">(choose second)</a></p>
-<select ui-select2 ng-model="select2">
-<option value="">Pick a number</option>
-<option value="one">First</option>
-<option value="two">Second</option>
-<option value="three">Third</option>
-</select>
-
-

Or try playing around with this sandbox demo to see how AJAX works

-
\ No newline at end of file diff --git a/bower_components/angular-ui-select2/docs/styles.css b/bower_components/angular-ui-select2/docs/styles.css deleted file mode 100644 index 08611a47..00000000 --- a/bower_components/angular-ui-select2/docs/styles.css +++ /dev/null @@ -1,4 +0,0 @@ - -#directives-select2 select { - width: 200px; -} \ No newline at end of file diff --git a/bower_components/angular-ui-select2/package.json b/bower_components/angular-ui-select2/package.json deleted file mode 100644 index 8bc18a23..00000000 --- a/bower_components/angular-ui-select2/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "author": "https://github.com/angular-ui/ui-select2/graphs/contributors", - "name": "angular-ui-select2", - "keywords": ["angular", "angularui", "select2"], - "description": "AngularUI - The companion suite for AngularJS", - "version": "0.0.2", - "homepage": "http://angular-ui.github.com", - "repository": { - "type": "git", - "url": "git://github.com/angular-ui/ui-select2.git" - }, - "engines": { - "node": ">= 0.8.4" - }, - "dependencies": {}, - "devDependencies": { - "grunt-recess": "~0.1.3", - "async": "0.1.x", - "karma": "~0" - } -} diff --git a/bower_components/angular-ui-select2/src/select2.js b/bower_components/angular-ui-select2/src/select2.js deleted file mode 100644 index 2b52471d..00000000 --- a/bower_components/angular-ui-select2/src/select2.js +++ /dev/null @@ -1,147 +0,0 @@ -/** - * Enhanced Select2 Dropmenus - * - * @AJAX Mode - When in this mode, your value will be an object (or array of objects) of the data used by Select2 - * This change is so that you do not have to do an additional query yourself on top of Select2's own query - * @params [options] {object} The configuration options passed to $.fn.select2(). Refer to the documentation - */ -angular.module('ui.select2', []).value('uiSelect2Config', {}).directive('uiSelect2', ['uiSelect2Config', '$timeout', function (uiSelect2Config, $timeout) { - var options = {}; - if (uiSelect2Config) { - angular.extend(options, uiSelect2Config); - } - return { - require: '?ngModel', - compile: function (tElm, tAttrs) { - var watch, - repeatOption, - repeatAttr, - isSelect = tElm.is('select'), - isMultiple = (tAttrs.multiple !== undefined); - - // Enable watching of the options dataset if in use - if (tElm.is('select')) { - repeatOption = tElm.find('option[ng-repeat], option[data-ng-repeat]'); - - if (repeatOption.length) { - repeatAttr = repeatOption.attr('ng-repeat') || repeatOption.attr('data-ng-repeat'); - watch = jQuery.trim(repeatAttr.split('|')[0]).split(' ').pop(); - } - } - - return function (scope, elm, attrs, controller) { - // instance-specific options - var opts = angular.extend({}, options, scope.$eval(attrs.uiSelect2)); - - if (isSelect) { - // Use element', function () { - describe('compiling this directive', function () { - it('should throw an error if we have no model defined', function () { - expect(function(){ - compile(''); - }).toThrow(); - }); - it('should create proper DOM structure', function () { - var element = compile(''); - expect(element.siblings().is('div.select2-container')).toBe(true); - }); - it('should not modify the model if there is no initial value', function(){ - //TODO - }); - }); - describe('when model is changed programmatically', function(){ - describe('for single select', function(){ - it('should set select2 to the value', function(){ - scope.foo = 'First'; - var element = compile(''); - expect(element.select2('val')).toBe('First'); - scope.$apply('foo = "Second"'); - expect(element.select2('val')).toBe('Second'); - }); - it('should handle falsey values', function(){ - scope.foo = 'First'; - var element = compile(''); - expect(element.select2('val')).toBe('First'); - scope.$apply('foo = false'); - expect(element.select2('val')).toBe(null); - scope.$apply('foo = "Second"'); - scope.$apply('foo = null'); - expect(element.select2('val')).toBe(null); - scope.$apply('foo = "Second"'); - scope.$apply('foo = undefined'); - expect(element.select2('val')).toBe(null); - }); - }); - describe('for multiple select', function(){ - it('should set select2 to multiple value', function(){ - scope.foo = ['First']; - var element = compile(''); - expect(element.select2('val')).toEqual(['First']); - scope.$apply('foo = ["Second"]'); - expect(element.select2('val')).toEqual(['Second']); - scope.$apply('foo = ["Second","Third"]'); - expect(element.select2('val')).toEqual(['Second','Third']); - }); - it('should handle falsey values', function(){ - scope.foo = ['First']; - var element = compile(''); - expect(element.val()).toEqual(['First']); - scope.$apply('foo = ["Second"]'); - scope.$apply('foo = false'); - expect(element.select2('val')).toEqual([]); - scope.$apply('foo = ["Second"]'); - scope.$apply('foo = null'); - expect(element.select2('val')).toEqual([]); - scope.$apply('foo = ["Second"]'); - scope.$apply('foo = undefined'); - expect(element.select2('val')).toEqual([]); - }); - }); - }); - it('should observe the disabled attribute', function () { - var element = compile(''); - expect(element.siblings().hasClass('select2-container-disabled')).toBe(false); - scope.$apply('disabled = true'); - expect(element.siblings().hasClass('select2-container-disabled')).toBe(true); - scope.$apply('disabled = false'); - expect(element.siblings().hasClass('select2-container-disabled')).toBe(false); - }); - it('should observe the multiple attribute', function () { - var element = $compile('')(scope); - - expect(element.siblings().hasClass('select2-container-multi')).toBe(false); - scope.$apply('multiple = true'); - expect(element.siblings().hasClass('select2-container-multi')).toBe(true); - scope.$apply('multiple = false'); - expect(element.siblings().hasClass('select2-container-multi')).toBe(false); - }); - it('should observe an option with ng-repeat for changes', function(){ - scope.items = ['first', 'second', 'third']; - scope.foo = 'fourth'; - var element = compile(''); - expect(element.select2('val')).toBe(null); - scope.$apply('foo="fourth";items=["fourth"]'); - $timeout.flush(); - expect(element.select2('val')).toBe('fourth'); - }); - }); - describe('with an element', function () { - describe('compiling this directive', function () { - it('should throw an error if we have no model defined', function () { - expect(function() { - compile(''); - }).toThrow(); - }); - it('should create proper DOM structure', function () { - var element = compile(''); - expect(element.siblings().is('div.select2-container')).toBe(true); - }); - it('should not modify the model if there is no initial value', function(){ - //TODO - }); - }); - describe('when model is changed programmatically', function(){ - describe('for single-select', function(){ - it('should call select2(data, ...) for objects', function(){ - var element = compile(''); - spyOn($.fn, 'select2'); - scope.$apply('foo={ id: 1, text: "first" }'); - expect(element.select2).toHaveBeenCalledWith('data', { id: 1, text: "first" }); - }); - it('should call select2(val, ...) for strings', function(){ - var element = compile(''); - spyOn($.fn, 'select2'); - scope.$apply('foo="first"'); - expect(element.select2).toHaveBeenCalledWith('val', 'first'); - }); - }); - describe('for multi-select', function(){ - it('should call select2(data, ...) for arrays', function(){ - var element = compile(''); - spyOn($.fn, 'select2'); - scope.$apply('foo=[{ id: 1, text: "first" },{ id: 2, text: "second" }]'); - expect(element.select2).toHaveBeenCalledWith('data', [{ id: 1, text: "first" },{ id: 2, text: "second" }]); - }); - it('should call select2(data, []) for falsey values', function(){ - var element = compile(''); - spyOn($.fn, 'select2'); - scope.$apply('foo=[]'); - expect(element.select2).toHaveBeenCalledWith('data', []); - }); - it('should call select2(val, ...) for strings', function(){ - var element = compile(''); - spyOn($.fn, 'select2'); - scope.$apply('foo="first,second"'); - expect(element.select2).toHaveBeenCalledWith('val', 'first,second'); - }); - }); - }); - describe('consumers of ngModel should correctly use $viewValue', function() { - it('should use any formatters if present (select - single select)', function(){ - scope.foo = 'First'; - var element = compile(''); - expect(element.select2('val')).toBe('First - I\'ve been formatted'); - scope.$apply('foo = "Second"'); - expect(element.select2('val')).toBe('Second - I\'ve been formatted'); - }); - - // isMultiple && falsey - it('should use any formatters if present (input multi select - falsey value)', function() { - // need special function to hit this case - // old code checked modelValue... can't just pass undefined to model value because view value will be the same - scope.transformers.fromModel = function(modelValue) { - if (modelValue === "magic") { - return undefined; - } - - return modelValue; - }; - - var element = compile(''); - spyOn($.fn, 'select2'); - scope.$apply('foo="magic"'); - expect(element.select2).toHaveBeenCalledWith('data', []); - }); - // isMultiple && isArray - it('should use any formatters if present (input multi select)', function() { - var element = compile(''); - spyOn($.fn, 'select2'); - scope.$apply('foo=[{ id: 1, text: "first" },{ id: 2, text: "second" }]'); - expect(element.select2).toHaveBeenCalledWith('data', [{ id: 1, text: "first - I've been formatted" },{ id: 2, text: "second - I've been formatted" }]); - }); - // isMultiple... - it('should use any formatters if present (input multi select - non array)', function() { - var element = compile(''); - spyOn($.fn, 'select2'); - scope.$apply('foo={ id: 1, text: "first" }'); - expect(element.select2).toHaveBeenCalledWith('val', { id: 1, text: "first - I've been formatted" }); - }); - - // !isMultiple - it('should use any formatters if present (input - single select - object)', function() { - var element = compile(''); - spyOn($.fn, 'select2'); - scope.$apply('foo={ id: 1, text: "first" }'); - expect(element.select2).toHaveBeenCalledWith('data', { id: 1, text: "first - I've been formatted" }); - }); - it('should use any formatters if present (input - single select - non object)', function() { - var element = compile(''); - spyOn($.fn, 'select2'); - scope.$apply('foo="first"'); - expect(element.select2).toHaveBeenCalledWith('val', "first - I've been formatted"); - }); - - it('should not set the default value using scope.$eval', function() { - // testing directive instantiation - change order of test - spyOn($.fn, 'select2'); - spyOn($.fn, 'val'); - scope.$apply('foo=[{ id: 1, text: "first" },{ id: 2, text: "second" }]'); - - var element = compile(''); - expect(element.val).not.toHaveBeenCalledWith([{ id: 1, text: "first" },{ id: 2, text: "second" }]); - }); - it('should expect a default value to be set with a call to the render method', function() { - // this should monitor the events after init, when the timeout callback executes - var opts = angular.copy(scope.options); - opts.multiple = true; - - scope.$apply('foo=[{ id: 1, text: "first" },{ id: 2, text: "second" }]'); - - spyOn($.fn, 'select2'); - var element = compile(''); - - // select 2 init - expect(element.select2).toHaveBeenCalledWith(opts); - - // callback setting - expect(element.select2).toHaveBeenCalledWith('data', [{ id: 1, text: "first - I've been formatted" },{ id: 2, text: "second - I've been formatted" }]); - - // retieve data - expect(element.select2).toHaveBeenCalledWith('data'); - }); - - }); - it('should set the model when the user selects an item', function(){ - var element = compile(''); - // TODO: programmactically select an option - // expect(scope.foo).toBe(/* selected val */) ; - }); - }); -}); \ No newline at end of file diff --git a/bower_components/angular-ui-select2/test/test.conf.js b/bower_components/angular-ui-select2/test/test.conf.js deleted file mode 100644 index ab117a69..00000000 --- a/bower_components/angular-ui-select2/test/test.conf.js +++ /dev/null @@ -1,20 +0,0 @@ -basePath = '..'; - -files = [ - JASMINE, - JASMINE_ADAPTER, - 'components/jquery/jquery.js', - 'components/angular/angular.js', - 'components/angular-mocks/angular-mocks.js', - 'components/select2/select2.js', - 'src/select2.js', - 'test/*Spec.js' -]; - -singleRun = false; - -autoWatch = true - -reporters = [ - 'dots' -]; \ No newline at end of file diff --git a/bower_components/angular/.bower.json b/bower_components/angular/.bower.json index e1aadf4d..dfcba4f6 100644 --- a/bower_components/angular/.bower.json +++ b/bower_components/angular/.bower.json @@ -1,16 +1,17 @@ { "name": "angular", - "version": "1.0.7", + "version": "1.2.27", "main": "./angular.js", + "ignore": [], "dependencies": {}, "homepage": "https://github.com/angular/bower-angular", - "_release": "1.0.7", + "_release": "1.2.27", "_resolution": { "type": "version", - "tag": "v1.0.7", - "commit": "6c0e81da2073f3831e32ed486d5aabe17bfc915f" + "tag": "v1.2.27", + "commit": "4429039fc57ddb810735179be1549c3bbb8e09a8" }, "_source": "git://github.com/angular/bower-angular.git", - "_target": "~1.0.7", + "_target": "~1.2.25", "_originalSource": "angular" } \ No newline at end of file diff --git a/bower_components/angular/angular.js b/bower_components/angular/angular.js index a860c859..34153eff 100644 --- a/bower_components/angular/angular.js +++ b/bower_components/angular/angular.js @@ -1,29 +1,208 @@ /** - * @license AngularJS v1.0.7 - * (c) 2010-2012 Google, Inc. http://angularjs.org + * @license AngularJS v1.2.27 + * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ -(function(window, document, undefined) { -'use strict'; +(function(window, document, undefined) {'use strict'; + +/** + * @description + * + * This object provides a utility for producing rich Error messages within + * Angular. It can be called as follows: + * + * var exampleMinErr = minErr('example'); + * throw exampleMinErr('one', 'This {0} is {1}', foo, bar); + * + * The above creates an instance of minErr in the example namespace. The + * resulting error will have a namespaced error code of example.one. The + * resulting error will replace {0} with the value of foo, and {1} with the + * value of bar. The object is not restricted in the number of arguments it can + * take. + * + * If fewer arguments are specified than necessary for interpolation, the extra + * interpolation markers will be preserved in the final string. + * + * Since data will be parsed statically during a build step, some restrictions + * are applied with respect to how minErr instances are created and called. + * Instances should have names of the form namespaceMinErr for a minErr created + * using minErr('namespace') . Error codes, namespaces and template strings + * should all be static strings, not variables or general expressions. + * + * @param {string} module The namespace to use for the new minErr instance. + * @returns {function(code:string, template:string, ...templateArgs): Error} minErr instance + */ + +function minErr(module) { + return function () { + var code = arguments[0], + prefix = '[' + (module ? module + ':' : '') + code + '] ', + template = arguments[1], + templateArgs = arguments, + stringify = function (obj) { + if (typeof obj === 'function') { + return obj.toString().replace(/ \{[\s\S]*$/, ''); + } else if (typeof obj === 'undefined') { + return 'undefined'; + } else if (typeof obj !== 'string') { + return JSON.stringify(obj); + } + return obj; + }, + message, i; + + message = prefix + template.replace(/\{\d+\}/g, function (match) { + var index = +match.slice(1, -1), arg; + + if (index + 2 < templateArgs.length) { + arg = templateArgs[index + 2]; + if (typeof arg === 'function') { + return arg.toString().replace(/ ?\{[\s\S]*$/, ''); + } else if (typeof arg === 'undefined') { + return 'undefined'; + } else if (typeof arg !== 'string') { + return toJson(arg); + } + return arg; + } + return match; + }); + + message = message + '\nhttp://errors.angularjs.org/1.2.27/' + + (module ? module + '/' : '') + code; + for (i = 2; i < arguments.length; i++) { + message = message + (i == 2 ? '?' : '&') + 'p' + (i-2) + '=' + + encodeURIComponent(stringify(arguments[i])); + } + + return new Error(message); + }; +} + +/* We need to tell jshint what variables are being exported */ +/* global angular: true, + msie: true, + jqLite: true, + jQuery: true, + slice: true, + push: true, + toString: true, + ngMinErr: true, + angularModule: true, + nodeName_: true, + uid: true, + VALIDITY_STATE_PROPERTY: true, + + lowercase: true, + uppercase: true, + manualLowercase: true, + manualUppercase: true, + nodeName_: true, + isArrayLike: true, + forEach: true, + sortedKeys: true, + forEachSorted: true, + reverseParams: true, + nextUid: true, + setHashKey: true, + extend: true, + int: true, + inherit: true, + noop: true, + identity: true, + valueFn: true, + isUndefined: true, + isDefined: true, + isObject: true, + isString: true, + isNumber: true, + isDate: true, + isArray: true, + isFunction: true, + isRegExp: true, + isWindow: true, + isScope: true, + isFile: true, + isBlob: true, + isBoolean: true, + isPromiseLike: true, + trim: true, + isElement: true, + makeMap: true, + map: true, + size: true, + includes: true, + indexOf: true, + arrayRemove: true, + isLeafNode: true, + copy: true, + shallowCopy: true, + equals: true, + csp: true, + concat: true, + sliceArgs: true, + bind: true, + toJsonReplacer: true, + toJson: true, + fromJson: true, + toBoolean: true, + startingTag: true, + tryDecodeURIComponent: true, + parseKeyValue: true, + toKeyValue: true, + encodeUriSegment: true, + encodeUriQuery: true, + angularInit: true, + bootstrap: true, + snake_case: true, + bindJQuery: true, + assertArg: true, + assertArgFn: true, + assertNotHasOwnProperty: true, + getter: true, + getBlockElements: true, + hasOwnProperty: true, +*/ //////////////////////////////////// +/** + * @ngdoc module + * @name ng + * @module ng + * @description + * + * # ng (core module) + * The ng module is loaded by default when an AngularJS application is started. The module itself + * contains the essential components for an AngularJS application to function. The table below + * lists a high level breakdown of each of the services/factories, filters, directives and testing + * components available within this core module. + * + *
+ */ + +// The name of a form control's ValidityState property. +// This is used so that it's possible for internal tests to create mock ValidityStates. +var VALIDITY_STATE_PROPERTY = 'validity'; + /** * @ngdoc function * @name angular.lowercase - * @function + * @module ng + * @kind function * * @description Converts the specified string to lowercase. * @param {string} string String to be converted to lowercase. * @returns {string} Lowercased string. */ var lowercase = function(string){return isString(string) ? string.toLowerCase() : string;}; - +var hasOwnProperty = Object.prototype.hasOwnProperty; /** * @ngdoc function * @name angular.uppercase - * @function + * @module ng + * @kind function * * @description Converts the specified string to uppercase. * @param {string} string String to be converted to uppercase. @@ -33,11 +212,13 @@ var uppercase = function(string){return isString(string) ? string.toUpperCase() var manualLowercase = function(s) { + /* jshint bitwise: false */ return isString(s) ? s.replace(/[A-Z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) | 32);}) : s; }; var manualUppercase = function(s) { + /* jshint bitwise: false */ return isString(s) ? s.replace(/[a-z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) & ~32);}) : s; @@ -54,12 +235,13 @@ if ('i' !== 'I'.toLowerCase()) { var /** holds major version number for IE or NaN for real browsers */ - msie = int((/msie (\d+)/.exec(lowercase(navigator.userAgent)) || [])[1]), + msie, jqLite, // delay binding since jQuery could be loaded after us. jQuery, // delay binding slice = [].slice, push = [].push, toString = Object.prototype.toString, + ngMinErr = minErr('ng'), /** @name angular */ angular = window.angular || (window.angular = {}), @@ -67,33 +249,42 @@ var /** holds major version number for IE or NaN for real browsers */ nodeName_, uid = ['0', '0', '0']; +/** + * IE 11 changed the format of the UserAgent string. + * See http://msdn.microsoft.com/en-us/library/ms537503.aspx + */ +msie = int((/msie (\d+)/.exec(lowercase(navigator.userAgent)) || [])[1]); +if (isNaN(msie)) { + msie = int((/trident\/.*; rv:(\d+)/.exec(lowercase(navigator.userAgent)) || [])[1]); +} + /** * @private * @param {*} obj - * @return {boolean} Returns true if `obj` is an array or array-like object (NodeList, Arguments, ...) + * @return {boolean} Returns true if `obj` is an array or array-like object (NodeList, Arguments, + * String ...) */ function isArrayLike(obj) { - if (!obj || (typeof obj.length !== 'number')) return false; + if (obj == null || isWindow(obj)) { + return false; + } + + var length = obj.length; - // We have on object which has length property. Should we treat it as array? - if (typeof obj.hasOwnProperty != 'function' && - typeof obj.constructor != 'function') { - // This is here for IE8: it is a bogus object treat it as array; + if (obj.nodeType === 1 && length) { return true; - } else { - return obj instanceof JQLite || // JQLite - (jQuery && obj instanceof jQuery) || // jQuery - toString.call(obj) !== '[object Object]' || // some browser native object - typeof obj.callee === 'function'; // arguments (on IE8 looks like regular obj) } -} + return isString(obj) || isArray(obj) || length === 0 || + typeof length === 'number' && length > 0 && (length - 1) in obj; +} /** * @ngdoc function * @name angular.forEach - * @function + * @module ng + * @kind function * * @description * Invokes the `iterator` function once for each item in `obj` collection, which can be either an @@ -101,16 +292,17 @@ function isArrayLike(obj) { * is the value of an object property or an array element and `key` is the object property key or * array element index. Specifying a `context` for the function is optional. * - * Note: this function was previously known as `angular.foreach`. + * It is worth noting that `.forEach` does not iterate over inherited properties because it filters + * using the `hasOwnProperty` method. * -
+   ```js
      var values = {name: 'misko', gender: 'male'};
      var log = [];
-     angular.forEach(values, function(value, key){
+     angular.forEach(values, function(value, key) {
        this.push(key + ': ' + value);
      }, log);
-     expect(log).toEqual(['name: misko', 'gender:male']);
-   
+ expect(log).toEqual(['name: misko', 'gender: male']); + ``` * * @param {Object|Array} obj Object to iterate over. * @param {Function} iterator Iterator function. @@ -120,17 +312,20 @@ function isArrayLike(obj) { function forEach(obj, iterator, context) { var key; if (obj) { - if (isFunction(obj)){ + if (isFunction(obj)) { for (key in obj) { - if (key != 'prototype' && key != 'length' && key != 'name' && obj.hasOwnProperty(key)) { + // Need to check if hasOwnProperty exists, + // as on IE8 the result of querySelectorAll is an object without a hasOwnProperty function + if (key != 'prototype' && key != 'length' && key != 'name' && (!obj.hasOwnProperty || obj.hasOwnProperty(key))) { iterator.call(context, obj[key], key); } } - } else if (obj.forEach && obj.forEach !== forEach) { - obj.forEach(iterator, context); - } else if (isArrayLike(obj)) { - for (key = 0; key < obj.length; key++) + } else if (isArray(obj) || isArrayLike(obj)) { + for (key = 0; key < obj.length; key++) { iterator.call(context, obj[key], key); + } + } else if (obj.forEach && obj.forEach !== forEach) { + obj.forEach(iterator, context); } else { for (key in obj) { if (obj.hasOwnProperty(key)) { @@ -167,7 +362,7 @@ function forEachSorted(obj, iterator, context) { * @returns {function(*, string)} */ function reverseParams(iteratorFn) { - return function(value, key) { iteratorFn(key, value) }; + return function(value, key) { iteratorFn(key, value); }; } /** @@ -176,7 +371,7 @@ function reverseParams(iteratorFn) { * the number string gets longer over time, and it can also overflow, where as the nextId * will grow much slower, it is a string, and it will never overflow. * - * @returns an unique alpha-numeric string + * @returns {string} an unique alpha-numeric string */ function nextUid() { var index = uid.length; @@ -203,7 +398,7 @@ function nextUid() { /** * Set or clear the hashkey for an object. - * @param obj object + * @param obj object * @param h the hashkey (!truthy to delete the hashkey) */ function setHashKey(obj, h) { @@ -218,10 +413,11 @@ function setHashKey(obj, h) { /** * @ngdoc function * @name angular.extend - * @function + * @module ng + * @kind function * * @description - * Extends the destination object `dst` by copying all of the properties from the `src` object(s) + * Extends the destination object `dst` by copying own enumerable properties from the `src` object(s) * to `dst`. You can specify multiple `src` objects. * * @param {Object} dst Destination object. @@ -230,9 +426,9 @@ function setHashKey(obj, h) { */ function extend(dst) { var h = dst.$$hashKey; - forEach(arguments, function(obj){ + forEach(arguments, function(obj) { if (obj !== dst) { - forEach(obj, function(value, key){ + forEach(obj, function(value, key) { dst[key] = value; }); } @@ -251,21 +447,21 @@ function inherit(parent, extra) { return extend(new (extend(function() {}, {prototype:parent}))(), extra); } - /** * @ngdoc function * @name angular.noop - * @function + * @module ng + * @kind function * * @description * A function that performs no operations. This function can be useful when writing code in the * functional style. -
+   ```js
      function foo(callback) {
        var result = calculateResult();
        (callback || angular.noop)(result);
      }
-   
+ ``` */ function noop() {} noop.$inject = []; @@ -274,17 +470,18 @@ noop.$inject = []; /** * @ngdoc function * @name angular.identity - * @function + * @module ng + * @kind function * * @description * A function that returns its first argument. This function is useful when writing code in the * functional style. * -
+   ```js
      function transformer(transformationFn, value) {
-       return (transformationFn || identity)(value);
+       return (transformationFn || angular.identity)(value);
      };
-   
+ ``` */ function identity($) {return $;} identity.$inject = []; @@ -295,7 +492,8 @@ function valueFn(value) {return function() {return value;};} /** * @ngdoc function * @name angular.isUndefined - * @function + * @module ng + * @kind function * * @description * Determines if a reference is undefined. @@ -303,13 +501,14 @@ function valueFn(value) {return function() {return value;};} * @param {*} value Reference to check. * @returns {boolean} True if `value` is undefined. */ -function isUndefined(value){return typeof value == 'undefined';} +function isUndefined(value){return typeof value === 'undefined';} /** * @ngdoc function * @name angular.isDefined - * @function + * @module ng + * @kind function * * @description * Determines if a reference is defined. @@ -317,28 +516,30 @@ function isUndefined(value){return typeof value == 'undefined';} * @param {*} value Reference to check. * @returns {boolean} True if `value` is defined. */ -function isDefined(value){return typeof value != 'undefined';} +function isDefined(value){return typeof value !== 'undefined';} /** * @ngdoc function * @name angular.isObject - * @function + * @module ng + * @kind function * * @description * Determines if a reference is an `Object`. Unlike `typeof` in JavaScript, `null`s are not - * considered to be objects. + * considered to be objects. Note that JavaScript arrays are objects. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Object` but not `null`. */ -function isObject(value){return value != null && typeof value == 'object';} +function isObject(value){return value != null && typeof value === 'object';} /** * @ngdoc function * @name angular.isString - * @function + * @module ng + * @kind function * * @description * Determines if a reference is a `String`. @@ -346,13 +547,14 @@ function isObject(value){return value != null && typeof value == 'object';} * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `String`. */ -function isString(value){return typeof value == 'string';} +function isString(value){return typeof value === 'string';} /** * @ngdoc function * @name angular.isNumber - * @function + * @module ng + * @kind function * * @description * Determines if a reference is a `Number`. @@ -360,13 +562,14 @@ function isString(value){return typeof value == 'string';} * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Number`. */ -function isNumber(value){return typeof value == 'number';} +function isNumber(value){return typeof value === 'number';} /** * @ngdoc function * @name angular.isDate - * @function + * @module ng + * @kind function * * @description * Determines if a value is a date. @@ -374,15 +577,16 @@ function isNumber(value){return typeof value == 'number';} * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Date`. */ -function isDate(value){ - return toString.apply(value) == '[object Date]'; +function isDate(value) { + return toString.call(value) === '[object Date]'; } /** * @ngdoc function * @name angular.isArray - * @function + * @module ng + * @kind function * * @description * Determines if a reference is an `Array`. @@ -390,15 +594,20 @@ function isDate(value){ * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Array`. */ -function isArray(value) { - return toString.apply(value) == '[object Array]'; -} - +var isArray = (function() { + if (!isFunction(Array.isArray)) { + return function(value) { + return toString.call(value) === '[object Array]'; + }; + } + return Array.isArray; +})(); /** * @ngdoc function * @name angular.isFunction - * @function + * @module ng + * @kind function * * @description * Determines if a reference is a `Function`. @@ -406,7 +615,19 @@ function isArray(value) { * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Function`. */ -function isFunction(value){return typeof value == 'function';} +function isFunction(value){return typeof value === 'function';} + + +/** + * Determines if a value is a regular expression object. + * + * @private + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is a `RegExp`. + */ +function isRegExp(value) { + return toString.call(value) === '[object RegExp]'; +} /** @@ -427,23 +648,45 @@ function isScope(obj) { function isFile(obj) { - return toString.apply(obj) === '[object File]'; + return toString.call(obj) === '[object File]'; +} + + +function isBlob(obj) { + return toString.call(obj) === '[object Blob]'; } function isBoolean(value) { - return typeof value == 'boolean'; + return typeof value === 'boolean'; } -function trim(value) { - return isString(value) ? value.replace(/^\s*/, '').replace(/\s*$/, '') : value; +function isPromiseLike(obj) { + return obj && isFunction(obj.then); } + +var trim = (function() { + // native trim is way faster: http://jsperf.com/angular-trim-test + // but IE doesn't have it... :-( + // TODO: we should move this into IE/ES5 polyfill + if (!String.prototype.trim) { + return function(value) { + return isString(value) ? value.replace(/^\s\s*/, '').replace(/\s\s*$/, '') : value; + }; + } + return function(value) { + return isString(value) ? value.trim() : value; + }; +})(); + + /** * @ngdoc function * @name angular.isElement - * @function + * @module ng + * @kind function * * @description * Determines if a reference is a DOM element (or wrapped jQuery element). @@ -452,16 +695,16 @@ function trim(value) { * @returns {boolean} True if `value` is a DOM element (or wrapped jQuery element). */ function isElement(node) { - return node && + return !!(node && (node.nodeName // we are a direct element - || (node.bind && node.find)); // we have a bind and find method part of jQuery API + || (node.prop && node.attr && node.find))); // we have an on and find method part of jQuery API } /** * @param str 'key1,key2,...' * @returns {object} in the form of {key1:true, key2:true, ...} */ -function makeMap(str){ +function makeMap(str) { var obj = {}, items = str.split(","), i; for ( i = 0; i < items.length; i++ ) obj[ items[i] ] = true; @@ -504,17 +747,17 @@ function map(obj, iterator, context) { * @returns {number} The size of `obj` or `0` if `obj` is neither an object nor an array. */ function size(obj, ownPropsOnly) { - var size = 0, key; + var count = 0, key; if (isArray(obj) || isString(obj)) { return obj.length; - } else if (isObject(obj)){ + } else if (isObject(obj)) { for (key in obj) if (!ownPropsOnly || obj.hasOwnProperty(key)) - size++; + count++; } - return size; + return count; } @@ -525,7 +768,7 @@ function includes(array, obj) { function indexOf(array, obj) { if (array.indexOf) return array.indexOf(obj); - for ( var i = 0; i < array.length; i++) { + for (var i = 0; i < array.length; i++) { if (obj === array[i]) return i; } return -1; @@ -553,7 +796,8 @@ function isLeafNode (node) { /** * @ngdoc function * @name angular.copy - * @function + * @module ng + * @kind function * * @description * Creates a deep copy of `source`, which should be an object or an array. @@ -561,83 +805,167 @@ function isLeafNode (node) { * * If no destination is supplied, a copy of the object or array is created. * * If a destination is provided, all of its elements (for array) or properties (for objects) * are deleted and then all elements/properties from the source are copied to it. - * * If `source` is not an object or array, `source` is returned. - * - * Note: this function is used to augment the Object type in Angular expressions. See - * {@link ng.$filter} for more information about Angular arrays. + * * If `source` is not an object or array (inc. `null` and `undefined`), `source` is returned. + * * If `source` is identical to 'destination' an exception will be thrown. * * @param {*} source The source that will be used to make a copy. * Can be any type, including primitives, `null`, and `undefined`. * @param {(Object|Array)=} destination Destination into which the source is copied. If * provided, must be of the same type as `source`. * @returns {*} The copy or updated `destination`, if `destination` was specified. + * + * @example + + +
+
+ Name:
+ E-mail:
+ Gender: male + female
+ + +
+
form = {{user | json}}
+
master = {{master | json}}
+
+ + +
+
*/ -function copy(source, destination){ - if (isWindow(source) || isScope(source)) throw Error("Can't copy Window or Scope"); +function copy(source, destination, stackSource, stackDest) { + if (isWindow(source) || isScope(source)) { + throw ngMinErr('cpws', + "Can't copy! Making copies of Window or Scope instances is not supported."); + } + if (!destination) { destination = source; if (source) { if (isArray(source)) { - destination = copy(source, []); + destination = copy(source, [], stackSource, stackDest); } else if (isDate(source)) { destination = new Date(source.getTime()); + } else if (isRegExp(source)) { + destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]); + destination.lastIndex = source.lastIndex; } else if (isObject(source)) { - destination = copy(source, {}); + destination = copy(source, {}, stackSource, stackDest); } } } else { - if (source === destination) throw Error("Can't copy equivalent objects or arrays"); + if (source === destination) throw ngMinErr('cpi', + "Can't copy! Source and destination are identical."); + + stackSource = stackSource || []; + stackDest = stackDest || []; + + if (isObject(source)) { + var index = indexOf(stackSource, source); + if (index !== -1) return stackDest[index]; + + stackSource.push(source); + stackDest.push(destination); + } + + var result; if (isArray(source)) { destination.length = 0; for ( var i = 0; i < source.length; i++) { - destination.push(copy(source[i])); + result = copy(source[i], null, stackSource, stackDest); + if (isObject(source[i])) { + stackSource.push(source[i]); + stackDest.push(result); + } + destination.push(result); } } else { var h = destination.$$hashKey; - forEach(destination, function(value, key){ - delete destination[key]; - }); + if (isArray(destination)) { + destination.length = 0; + } else { + forEach(destination, function(value, key) { + delete destination[key]; + }); + } for ( var key in source) { - destination[key] = copy(source[key]); + result = copy(source[key], null, stackSource, stackDest); + if (isObject(source[key])) { + stackSource.push(source[key]); + stackDest.push(result); + } + destination[key] = result; } setHashKey(destination,h); } + } return destination; } /** - * Create a shallow copy of an object + * Creates a shallow copy of an object, an array or a primitive */ function shallowCopy(src, dst) { - dst = dst || {}; + if (isArray(src)) { + dst = dst || []; - for(var key in src) { - if (src.hasOwnProperty(key) && key.substr(0, 2) !== '$$') { - dst[key] = src[key]; + for ( var i = 0; i < src.length; i++) { + dst[i] = src[i]; + } + } else if (isObject(src)) { + dst = dst || {}; + + for (var key in src) { + if (hasOwnProperty.call(src, key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) { + dst[key] = src[key]; + } } } - return dst; + return dst || src; } /** * @ngdoc function * @name angular.equals - * @function + * @module ng + * @kind function * * @description - * Determines if two objects or two values are equivalent. Supports value types, arrays and - * objects. + * Determines if two objects or two values are equivalent. Supports value types, regular + * expressions, arrays and objects. * * Two objects or values are considered equivalent if at least one of the following is true: * * * Both objects or values pass `===` comparison. - * * Both objects or values are of the same type and all of their properties pass `===` comparison. - * * Both values are NaN. (In JavasScript, NaN == NaN => false. But we consider two NaN as equal) - * - * During a property comparision, properties of `function` type and properties with names + * * Both objects or values are of the same type and all of their properties are equal by + * comparing them with `angular.equals`. + * * Both values are NaN. (In JavaScript, NaN == NaN => false. But we consider two NaN as equal) + * * Both values represent the same regular expression (In JavaScript, + * /abc/ == /abc/ => false. But we consider two regular expressions as equal when their textual + * representation matches). + * + * During a property comparison, properties of `function` type and properties with names * that begin with `$` are ignored. * * Scope and DOMWindow objects are being compared only by identify (`===`). @@ -654,6 +982,7 @@ function equals(o1, o2) { if (t1 == t2) { if (t1 == 'object') { if (isArray(o1)) { + if (!isArray(o2)) return false; if ((length = o1.length) == o2.length) { for(key=0; key 2 ? sliceArgs(arguments, 2) : []; if (isFunction(fn) && !(fn instanceof RegExp)) { @@ -732,7 +1088,7 @@ function bind(self, fn) { function toJsonReplacer(key, value) { var val = value; - if (/^\$+/.test(key)) { + if (typeof key === 'string' && key.charAt(0) === '$') { val = undefined; } else if (isWindow(value)) { val = '$WINDOW'; @@ -749,16 +1105,19 @@ function toJsonReplacer(key, value) { /** * @ngdoc function * @name angular.toJson - * @function + * @module ng + * @kind function * * @description - * Serializes input into a JSON-formatted string. + * Serializes input into a JSON-formatted string. Properties with leading $ characters will be + * stripped since angular uses this notation internally. * * @param {Object|Array|Date|string|number} obj Input to be serialized into JSON. * @param {boolean=} pretty If set to true, the JSON output will contain newlines and whitespace. - * @returns {string} Jsonified string representing `obj`. + * @returns {string|undefined} JSON-ified string representing `obj`. */ function toJson(obj, pretty) { + if (typeof obj === 'undefined') return undefined; return JSON.stringify(obj, toJsonReplacer, pretty ? ' ' : null); } @@ -766,13 +1125,14 @@ function toJson(obj, pretty) { /** * @ngdoc function * @name angular.fromJson - * @function + * @module ng + * @kind function * * @description * Deserializes a JSON string. * * @param {string} json JSON string to deserialize. - * @returns {Object|Array|Date|string|number} Deserialized thingy. + * @returns {Object|Array|string|number} Deserialized thingy. */ function fromJson(json) { return isString(json) @@ -782,7 +1142,9 @@ function fromJson(json) { function toBoolean(value) { - if (value && value.length !== 0) { + if (typeof value === 'function') { + value = true; + } else if (value && value.length !== 0) { var v = lowercase("" + value); value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == 'n' || v == '[]'); } else { @@ -799,7 +1161,7 @@ function startingTag(element) { try { // turns out IE does not let you set .html() on elements which // are not allowed to have children. So we just ignore it. - element.html(''); + element.empty(); } catch(e) {} // As Per DOM Standards var TEXT_NODE = 3; @@ -818,17 +1180,43 @@ function startingTag(element) { ///////////////////////////////////////////////// +/** + * Tries to decode the URI component without throwing an exception. + * + * @private + * @param str value potential URI component to check. + * @returns {boolean} True if `value` can be decoded + * with the decodeURIComponent function. + */ +function tryDecodeURIComponent(value) { + try { + return decodeURIComponent(value); + } catch(e) { + // Ignore any invalid uri component + } +} + + /** * Parses an escaped url query string into key-value pairs. - * @returns Object.<(string|boolean)> + * @returns {Object.} */ function parseKeyValue(/**string*/keyValue) { var obj = {}, key_value, key; - forEach((keyValue || "").split('&'), function(keyValue){ - if (keyValue) { - key_value = keyValue.split('='); - key = decodeURIComponent(key_value[0]); - obj[key] = isDefined(key_value[1]) ? decodeURIComponent(key_value[1]) : true; + forEach((keyValue || "").split('&'), function(keyValue) { + if ( keyValue ) { + key_value = keyValue.replace(/\+/g,'%20').split('='); + key = tryDecodeURIComponent(key_value[0]); + if ( isDefined(key) ) { + var val = isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true; + if (!hasOwnProperty.call(obj, key)) { + obj[key] = val; + } else if(isArray(obj[key])) { + obj[key].push(val); + } else { + obj[key] = [obj[key],val]; + } + } } }); return obj; @@ -837,14 +1225,22 @@ function parseKeyValue(/**string*/keyValue) { function toKeyValue(obj) { var parts = []; forEach(obj, function(value, key) { - parts.push(encodeUriQuery(key, true) + (value === true ? '' : '=' + encodeUriQuery(value, true))); + if (isArray(value)) { + forEach(value, function(arrayValue) { + parts.push(encodeUriQuery(key, true) + + (arrayValue === true ? '' : '=' + encodeUriQuery(arrayValue, true))); + }); + } else { + parts.push(encodeUriQuery(key, true) + + (value === true ? '' : '=' + encodeUriQuery(value, true))); + } }); return parts.length ? parts.join('&') : ''; } /** - * We need our custom method because encodeURIComponent is too agressive and doesn't follow + * We need our custom method because encodeURIComponent is too aggressive and doesn't follow * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path * segments: * segment = *pchar @@ -864,7 +1260,7 @@ function encodeUriSegment(val) { /** * This method is intended for encoding *key* or *value* parts of query component. We need a custom - * method becuase encodeURIComponent is too agressive and encodes stuff that doesn't have to be + * method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be * encoded per http://tools.ietf.org/html/rfc3986: * query = *( pchar / "/" / "?" ) * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" @@ -885,7 +1281,8 @@ function encodeUriQuery(val, pctEncodeSpaces) { /** * @ngdoc directive - * @name ng.directive:ngApp + * @name ngApp + * @module ng * * @element ANY * @param {angular.Module} ngApp an optional application @@ -893,22 +1290,39 @@ function encodeUriQuery(val, pctEncodeSpaces) { * * @description * - * Use this directive to auto-bootstrap an application. Only - * one directive can be used per HTML document. The directive - * designates the root of the application and is typically placed - * at the root of the page. - * - * In the example below if the `ngApp` directive would not be placed - * on the `html` element then the document would not be compiled - * and the `{{ 1+2 }}` would not be resolved to `3`. - * - * `ngApp` is the easiest way to bootstrap an application. - * - - - I can add: 1 + 2 = {{ 1+2 }} - - + * Use this directive to **auto-bootstrap** an AngularJS application. The `ngApp` directive + * designates the **root element** of the application and is typically placed near the root element + * of the page - e.g. on the `` or `` tags. + * + * Only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp` + * found in the document will be used to define the root element to auto-bootstrap as an + * application. To run multiple applications in an HTML document you must manually bootstrap them using + * {@link angular.bootstrap} instead. AngularJS applications cannot be nested within each other. + * + * You can specify an **AngularJS module** to be used as the root module for the application. This + * module will be loaded into the {@link auto.$injector} when the application is bootstrapped and + * should contain the application code needed or have dependencies on other modules that will + * contain the code. See {@link angular.module} for more information. + * + * In the example below if the `ngApp` directive were not placed on the `html` element then the + * document would not be compiled, the `AppController` would not be instantiated and the `{{ a+b }}` + * would not be resolved to `3`. + * + * `ngApp` is the easiest, and most common, way to bootstrap an application. + * + + +
+ I can add: {{a}} + {{b}} = {{ a+b }} +
+
+ + angular.module('ngAppDemo', []).controller('ngAppDemoController', function($scope) { + $scope.a = 1; + $scope.b = 2; + }); + +
* */ function angularInit(element, bootstrap) { @@ -958,26 +1372,78 @@ function angularInit(element, bootstrap) { /** * @ngdoc function * @name angular.bootstrap + * @module ng * @description * Use this function to manually start up angular application. * * See: {@link guide/bootstrap Bootstrap} * - * @param {Element} element DOM element which is the root of angular application. - * @param {Array=} modules an array of module declarations. See: {@link angular.module modules} - * @returns {AUTO.$injector} Returns the newly created injector for this app. + * Note that ngScenario-based end-to-end tests cannot use this function to bootstrap manually. + * They must use {@link ng.directive:ngApp ngApp}. + * + * Angular will detect if it has been loaded into the browser more than once and only allow the + * first loaded script to be bootstrapped and will report a warning to the browser console for + * each of the subsequent scripts. This prevents strange results in applications, where otherwise + * multiple instances of Angular try to work on the DOM. + * + * + * + * + *
+ * + * + * + * + * + * + * + *
{{heading}}
{{fill}}
+ *
+ *
+ * + * var app = angular.module('multi-bootstrap', []) + * + * .controller('BrokenTable', function($scope) { + * $scope.headings = ['One', 'Two', 'Three']; + * $scope.fillings = [[1, 2, 3], ['A', 'B', 'C'], [7, 8, 9]]; + * }); + * + * + * it('should only insert one table cell for each item in $scope.fillings', function() { + * expect(element.all(by.css('td')).count()) + * .toBe(9); + * }); + * + *
+ * + * @param {DOMElement} element DOM element which is the root of angular application. + * @param {Array=} modules an array of modules to load into the application. + * Each item in the array should be the name of a predefined module or a (DI annotated) + * function that will be invoked by the injector as a run block. + * See: {@link angular.module modules} + * @returns {auto.$injector} Returns the newly created injector for this app. */ function bootstrap(element, modules) { - var resumeBootstrapInternal = function() { + var doBootstrap = function() { element = jqLite(element); + + if (element.injector()) { + var tag = (element[0] === document) ? 'document' : startingTag(element); + //Encode angle brackets to prevent input from being sanitized to empty string #8683 + throw ngMinErr( + 'btstrpd', + "App Already Bootstrapped with this Element '{0}'", + tag.replace(//,'>')); + } + modules = modules || []; modules.unshift(['$provide', function($provide) { $provide.value('$rootElement', element); }]); modules.unshift('ng'); var injector = createInjector(modules); - injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', - function(scope, element, compile, injector) { + injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate', + function(scope, element, compile, injector, animate) { scope.$apply(function() { element.data('$injector', injector); compile(element)(scope); @@ -990,7 +1456,7 @@ function bootstrap(element, modules) { var NG_DEFER_BOOTSTRAP = /^NG_DEFER_BOOTSTRAP!/; if (window && !NG_DEFER_BOOTSTRAP.test(window.name)) { - return resumeBootstrapInternal(); + return doBootstrap(); } window.name = window.name.replace(NG_DEFER_BOOTSTRAP, ''); @@ -998,12 +1464,12 @@ function bootstrap(element, modules) { forEach(extraModules, function(module) { modules.push(module); }); - resumeBootstrapInternal(); + doBootstrap(); }; } var SNAKE_CASE_REGEXP = /[A-Z]/g; -function snake_case(name, separator){ +function snake_case(name, separator) { separator = separator || '_'; return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) { return (pos ? separator : '') + letter.toLowerCase(); @@ -1013,18 +1479,22 @@ function snake_case(name, separator){ function bindJQuery() { // bind to jQuery if present; jQuery = window.jQuery; - // reset to jQuery or default to us. - if (jQuery) { + // Use jQuery if it exists with proper functionality, otherwise default to us. + // Angular 1.2+ requires jQuery 1.7.1+ for on()/off() support. + if (jQuery && jQuery.fn.on) { jqLite = jQuery; extend(jQuery.fn, { scope: JQLitePrototype.scope, + isolateScope: JQLitePrototype.isolateScope, controller: JQLitePrototype.controller, injector: JQLitePrototype.injector, inheritedData: JQLitePrototype.inheritedData }); - JQLitePatchJQueryRemove('remove', true); - JQLitePatchJQueryRemove('empty'); - JQLitePatchJQueryRemove('html'); + // Method signature: + // jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) + jqLitePatchJQueryRemove('remove', true, true, false); + jqLitePatchJQueryRemove('empty', false, false, false); + jqLitePatchJQueryRemove('html', false, false, true); } else { jqLite = JQLite; } @@ -1036,7 +1506,7 @@ function bindJQuery() { */ function assertArg(arg, name, reason) { if (!arg) { - throw new Error("Argument '" + (name || '?') + "' is " + (reason || "required")); + throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required")); } return arg; } @@ -1047,13 +1517,76 @@ function assertArgFn(arg, name, acceptArrayAnnotation) { } assertArg(isFunction(arg), name, 'not a function, got ' + - (arg && typeof arg == 'object' ? arg.constructor.name || 'Object' : typeof arg)); + (arg && typeof arg === 'object' ? arg.constructor.name || 'Object' : typeof arg)); return arg; } /** - * @ngdoc interface + * throw error if the name given is hasOwnProperty + * @param {String} name the name to test + * @param {String} context the context in which the name is used, such as module or directive + */ +function assertNotHasOwnProperty(name, context) { + if (name === 'hasOwnProperty') { + throw ngMinErr('badname', "hasOwnProperty is not a valid {0} name", context); + } +} + +/** + * Return the value accessible from the object by path. Any undefined traversals are ignored + * @param {Object} obj starting object + * @param {String} path path to traverse + * @param {boolean} [bindFnToScope=true] + * @returns {Object} value as accessible by path + */ +//TODO(misko): this function needs to be removed +function getter(obj, path, bindFnToScope) { + if (!path) return obj; + var keys = path.split('.'); + var key; + var lastInstance = obj; + var len = keys.length; + + for (var i = 0; i < len; i++) { + key = keys[i]; + if (obj) { + obj = (lastInstance = obj)[key]; + } + } + if (!bindFnToScope && isFunction(obj)) { + return bind(lastInstance, obj); + } + return obj; +} + +/** + * Return the DOM siblings between the first and last node in the given array. + * @param {Array} array like object + * @returns {DOMElement} object containing the elements + */ +function getBlockElements(nodes) { + var startNode = nodes[0], + endNode = nodes[nodes.length - 1]; + if (startNode === endNode) { + return jqLite(startNode); + } + + var element = startNode; + var elements = [element]; + + do { + element = element.nextSibling; + if (!element) break; + elements.push(element); + } while (element !== endNode); + + return jqLite(elements); +} + +/** + * @ngdoc type * @name angular.Module + * @module ng * @description * * Interface for configuring angular {@link angular.module modules}. @@ -1061,30 +1594,43 @@ function assertArgFn(arg, name, acceptArrayAnnotation) { function setupModuleLoader(window) { + var $injectorMinErr = minErr('$injector'); + var ngMinErr = minErr('ng'); + function ensure(obj, name, factory) { return obj[name] || (obj[name] = factory()); } - return ensure(ensure(window, 'angular', Object), 'module', function() { + var angular = ensure(window, 'angular', Object); + + // We need to expose `angular.$$minErr` to modules such as `ngResource` that reference it during bootstrap + angular.$$minErr = angular.$$minErr || minErr; + + return ensure(angular, 'module', function() { /** @type {Object.} */ var modules = {}; /** * @ngdoc function * @name angular.module + * @module ng * @description * - * The `angular.module` is a global place for creating and registering Angular modules. All - * modules (angular core or 3rd party) that should be available to an application must be + * The `angular.module` is a global place for creating, registering and retrieving Angular + * modules. + * All modules (angular core or 3rd party) that should be available to an application must be * registered using this mechanism. * + * When passed two or more arguments, a new module is created. If passed only one argument, an + * existing module (the name passed as the first argument to `module`) is retrieved. + * * * # Module * - * A module is a collocation of services, directives, filters, and configuration information. Module - * is used to configure the {@link AUTO.$injector $injector}. + * A module is a collection of services, directives, controllers, filters, and configuration information. + * `angular.module` is used to configure the {@link auto.$injector $injector}. * - *
+     * ```js
      * // Create a new module
      * var myModule = angular.module('myModule', []);
      *
@@ -1092,36 +1638,45 @@ function setupModuleLoader(window) {
      * myModule.value('appName', 'MyCoolApp');
      *
      * // configure existing services inside initialization blocks.
-     * myModule.config(function($locationProvider) {
+     * myModule.config(['$locationProvider', function($locationProvider) {
      *   // Configure existing providers
      *   $locationProvider.hashPrefix('!');
-     * });
-     * 
+ * }]); + * ``` * * Then you can create an injector and load your modules like this: * - *
-     * var injector = angular.injector(['ng', 'MyModule'])
-     * 
+ * ```js + * var injector = angular.injector(['ng', 'myModule']) + * ``` * * However it's more likely that you'll just use * {@link ng.directive:ngApp ngApp} or * {@link angular.bootstrap} to simplify this process for you. * * @param {!string} name The name of the module to create or retrieve. - * @param {Array.=} requires If specified then new module is being created. If unspecified then the - * the module is being retrieved for further configuration. - * @param {Function} configFn Optional configuration function for the module. Same as + * @param {!Array.=} requires If specified then new module is being created. If + * unspecified then the module is being retrieved for further configuration. + * @param {Function=} configFn Optional configuration function for the module. Same as * {@link angular.Module#config Module#config()}. * @returns {module} new module with the {@link angular.Module} api. */ return function module(name, requires, configFn) { + var assertNotHasOwnProperty = function(name, context) { + if (name === 'hasOwnProperty') { + throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context); + } + }; + + assertNotHasOwnProperty(name, 'module'); if (requires && modules.hasOwnProperty(name)) { modules[name] = null; } return ensure(modules, name, function() { if (!requires) { - throw Error('No module: ' + name); + throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled " + + "the module name or forgot to load it. If registering a module ensure that you " + + "specify the dependencies as the second argument.", name); } /** @type {!Array.>} */ @@ -1141,19 +1696,21 @@ function setupModuleLoader(window) { /** * @ngdoc property * @name angular.Module#requires - * @propertyOf angular.Module - * @returns {Array.} List of module names which must be loaded before this module. + * @module ng + * * @description - * Holds the list of modules which the injector will load before the current module is loaded. + * Holds the list of modules which the injector will load before the current module is + * loaded. */ requires: requires, /** * @ngdoc property * @name angular.Module#name - * @propertyOf angular.Module - * @returns {string} Name of the module. + * @module ng + * * @description + * Name of the module. */ name: name, @@ -1161,63 +1718,98 @@ function setupModuleLoader(window) { /** * @ngdoc method * @name angular.Module#provider - * @methodOf angular.Module + * @module ng * @param {string} name service name - * @param {Function} providerType Construction function for creating new instance of the service. + * @param {Function} providerType Construction function for creating new instance of the + * service. * @description - * See {@link AUTO.$provide#provider $provide.provider()}. + * See {@link auto.$provide#provider $provide.provider()}. */ provider: invokeLater('$provide', 'provider'), /** * @ngdoc method * @name angular.Module#factory - * @methodOf angular.Module + * @module ng * @param {string} name service name * @param {Function} providerFunction Function for creating new instance of the service. * @description - * See {@link AUTO.$provide#factory $provide.factory()}. + * See {@link auto.$provide#factory $provide.factory()}. */ factory: invokeLater('$provide', 'factory'), /** * @ngdoc method * @name angular.Module#service - * @methodOf angular.Module + * @module ng * @param {string} name service name * @param {Function} constructor A constructor function that will be instantiated. * @description - * See {@link AUTO.$provide#service $provide.service()}. + * See {@link auto.$provide#service $provide.service()}. */ service: invokeLater('$provide', 'service'), /** * @ngdoc method * @name angular.Module#value - * @methodOf angular.Module + * @module ng * @param {string} name service name * @param {*} object Service instance object. * @description - * See {@link AUTO.$provide#value $provide.value()}. + * See {@link auto.$provide#value $provide.value()}. */ value: invokeLater('$provide', 'value'), /** * @ngdoc method * @name angular.Module#constant - * @methodOf angular.Module + * @module ng * @param {string} name constant name * @param {*} object Constant value. * @description * Because the constant are fixed, they get applied before other provide methods. - * See {@link AUTO.$provide#constant $provide.constant()}. + * See {@link auto.$provide#constant $provide.constant()}. */ constant: invokeLater('$provide', 'constant', 'unshift'), + /** + * @ngdoc method + * @name angular.Module#animation + * @module ng + * @param {string} name animation name + * @param {Function} animationFactory Factory function for creating new instance of an + * animation. + * @description + * + * **NOTE**: animations take effect only if the **ngAnimate** module is loaded. + * + * + * Defines an animation hook that can be later used with + * {@link ngAnimate.$animate $animate} service and directives that use this service. + * + * ```js + * module.animation('.animation-name', function($inject1, $inject2) { + * return { + * eventName : function(element, done) { + * //code to run the animation + * //once complete, then run done() + * return function cancellationFunction(element) { + * //code to cancel the animation + * } + * } + * } + * }) + * ``` + * + * See {@link ngAnimate.$animateProvider#register $animateProvider.register()} and + * {@link ngAnimate ngAnimate module} for more information. + */ + animation: invokeLater('$animateProvider', 'register'), + /** * @ngdoc method * @name angular.Module#filter - * @methodOf angular.Module + * @module ng * @param {string} name Filter name. * @param {Function} filterFactory Factory function for creating new instance of filter. * @description @@ -1228,8 +1820,9 @@ function setupModuleLoader(window) { /** * @ngdoc method * @name angular.Module#controller - * @methodOf angular.Module - * @param {string} name Controller name. + * @module ng + * @param {string|Object} name Controller name, or an object map of controllers where the + * keys are the names and the values are the constructors. * @param {Function} constructor Controller constructor function. * @description * See {@link ng.$controllerProvider#register $controllerProvider.register()}. @@ -1239,8 +1832,9 @@ function setupModuleLoader(window) { /** * @ngdoc method * @name angular.Module#directive - * @methodOf angular.Module - * @param {string} name directive name + * @module ng + * @param {string|Object} name Directive name, or an object map of directives where the + * keys are the names and the values are the factories. * @param {Function} directiveFactory Factory function for creating new instance of * directives. * @description @@ -1251,18 +1845,20 @@ function setupModuleLoader(window) { /** * @ngdoc method * @name angular.Module#config - * @methodOf angular.Module + * @module ng * @param {Function} configFn Execute this function on module load. Useful for service * configuration. * @description * Use this method to register work which needs to be performed on module loading. + * For more about how to configure services, see + * {@link providers#providers_provider-recipe Provider Recipe}. */ config: config, /** * @ngdoc method * @name angular.Module#run - * @methodOf angular.Module + * @module ng * @param {Function} initializationFn Execute this function after injector creation. * Useful for application initialization. * @description @@ -1291,7 +1887,7 @@ function setupModuleLoader(window) { return function() { invokeQueue[insertMethod || 'push']([provider, method, arguments]); return moduleInstance; - } + }; } }); }; @@ -1299,9 +1895,87 @@ function setupModuleLoader(window) { } +/* global angularModule: true, + version: true, + + $LocaleProvider, + $CompileProvider, + + htmlAnchorDirective, + inputDirective, + inputDirective, + formDirective, + scriptDirective, + selectDirective, + styleDirective, + optionDirective, + ngBindDirective, + ngBindHtmlDirective, + ngBindTemplateDirective, + ngClassDirective, + ngClassEvenDirective, + ngClassOddDirective, + ngCspDirective, + ngCloakDirective, + ngControllerDirective, + ngFormDirective, + ngHideDirective, + ngIfDirective, + ngIncludeDirective, + ngIncludeFillContentDirective, + ngInitDirective, + ngNonBindableDirective, + ngPluralizeDirective, + ngRepeatDirective, + ngShowDirective, + ngStyleDirective, + ngSwitchDirective, + ngSwitchWhenDirective, + ngSwitchDefaultDirective, + ngOptionsDirective, + ngTranscludeDirective, + ngModelDirective, + ngListDirective, + ngChangeDirective, + requiredDirective, + requiredDirective, + ngValueDirective, + ngAttributeAliasDirectives, + ngEventDirectives, + + $AnchorScrollProvider, + $AnimateProvider, + $BrowserProvider, + $CacheFactoryProvider, + $ControllerProvider, + $DocumentProvider, + $ExceptionHandlerProvider, + $FilterProvider, + $InterpolateProvider, + $IntervalProvider, + $HttpProvider, + $HttpBackendProvider, + $LocationProvider, + $LogProvider, + $ParseProvider, + $RootScopeProvider, + $QProvider, + $$SanitizeUriProvider, + $SceProvider, + $SceDelegateProvider, + $SnifferProvider, + $TemplateCacheProvider, + $TimeoutProvider, + $$RAFProvider, + $$AsyncCallbackProvider, + $WindowProvider +*/ + + /** - * @ngdoc property + * @ngdoc object * @name angular.version + * @module ng * @description * An object that contains information about the current AngularJS version. This object has the * following properties: @@ -1313,11 +1987,11 @@ function setupModuleLoader(window) { * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". */ var version = { - full: '1.0.7', // all of these placeholder strings will be replaced by grunt's + full: '1.2.27', // all of these placeholder strings will be replaced by grunt's major: 1, // package task - minor: 0, - dot: 7, - codeName: 'monochromatic-rainbow' + minor: 2, + dot: 27, + codeName: 'prime-factorization' }; @@ -1330,11 +2004,11 @@ function publishExternalAPI(angular){ 'element': jqLite, 'forEach': forEach, 'injector': createInjector, - 'noop':noop, - 'bind':bind, + 'noop': noop, + 'bind': bind, 'toJson': toJson, 'fromJson': fromJson, - 'identity':identity, + 'identity': identity, 'isUndefined': isUndefined, 'isDefined': isDefined, 'isString': isString, @@ -1347,7 +2021,9 @@ function publishExternalAPI(angular){ 'isDate': isDate, 'lowercase': lowercase, 'uppercase': uppercase, - 'callbacks': {counter: 0} + 'callbacks': {counter: 0}, + '$$minErr': minErr, + '$$csp': csp }); angularModule = setupModuleLoader(window); @@ -1359,6 +2035,10 @@ function publishExternalAPI(angular){ angularModule('ng', ['ngLocale'], ['$provide', function ngModule($provide) { + // $$sanitizeUriProvider needs to be before $compileProvider as it is used by it. + $provide.provider({ + $$sanitizeUri: $$SanitizeUriProvider + }); $provide.provider('$compile', $CompileProvider). directive({ a: htmlAnchorDirective, @@ -1370,29 +2050,27 @@ function publishExternalAPI(angular){ style: styleDirective, option: optionDirective, ngBind: ngBindDirective, - ngBindHtmlUnsafe: ngBindHtmlUnsafeDirective, + ngBindHtml: ngBindHtmlDirective, ngBindTemplate: ngBindTemplateDirective, ngClass: ngClassDirective, ngClassEven: ngClassEvenDirective, ngClassOdd: ngClassOddDirective, - ngCsp: ngCspDirective, ngCloak: ngCloakDirective, ngController: ngControllerDirective, ngForm: ngFormDirective, ngHide: ngHideDirective, + ngIf: ngIfDirective, ngInclude: ngIncludeDirective, ngInit: ngInitDirective, ngNonBindable: ngNonBindableDirective, ngPluralize: ngPluralizeDirective, ngRepeat: ngRepeatDirective, ngShow: ngShowDirective, - ngSubmit: ngSubmitDirective, ngStyle: ngStyleDirective, ngSwitch: ngSwitchDirective, ngSwitchWhen: ngSwitchWhenDirective, ngSwitchDefault: ngSwitchDefaultDirective, ngOptions: ngOptionsDirective, - ngView: ngViewDirective, ngTransclude: ngTranscludeDirective, ngModel: ngModelDirective, ngList: ngListDirective, @@ -1401,10 +2079,14 @@ function publishExternalAPI(angular){ ngRequired: requiredDirective, ngValue: ngValueDirective }). + directive({ + ngInclude: ngIncludeFillContentDirective + }). directive(ngAttributeAliasDirectives). directive(ngEventDirectives); $provide.provider({ $anchorScroll: $AnchorScrollProvider, + $animate: $AnimateProvider, $browser: $BrowserProvider, $cacheFactory: $CacheFactoryProvider, $controller: $ControllerProvider, @@ -1412,24 +2094,33 @@ function publishExternalAPI(angular){ $exceptionHandler: $ExceptionHandlerProvider, $filter: $FilterProvider, $interpolate: $InterpolateProvider, + $interval: $IntervalProvider, $http: $HttpProvider, $httpBackend: $HttpBackendProvider, $location: $LocationProvider, $log: $LogProvider, $parse: $ParseProvider, - $route: $RouteProvider, - $routeParams: $RouteParamsProvider, $rootScope: $RootScopeProvider, $q: $QProvider, + $sce: $SceProvider, + $sceDelegate: $SceDelegateProvider, $sniffer: $SnifferProvider, $templateCache: $TemplateCacheProvider, $timeout: $TimeoutProvider, - $window: $WindowProvider + $window: $WindowProvider, + $$rAF: $$RAFProvider, + $$asyncCallback : $$AsyncCallbackProvider }); } ]); } +/* global JQLitePrototype: true, + addEventListenerFn: true, + removeEventListenerFn: true, + BOOLEAN_ATTR: true +*/ + ////////////////////////////////// //JQLite ////////////////////////////////// @@ -1437,67 +2128,82 @@ function publishExternalAPI(angular){ /** * @ngdoc function * @name angular.element - * @function + * @module ng + * @kind function * * @description * Wraps a raw DOM element or HTML string as a [jQuery](http://jquery.com) element. - * `angular.element` can be either an alias for [jQuery](http://api.jquery.com/jQuery/) function, if - * jQuery is available, or a function that wraps the element or string in Angular's jQuery lite - * implementation (commonly referred to as jqLite). - * - * Real jQuery always takes precedence over jqLite, provided it was loaded before `DOMContentLoaded` - * event fired. - * - * jqLite is a tiny, API-compatible subset of jQuery that allows - * Angular to manipulate the DOM. jqLite implements only the most commonly needed functionality - * within a very small footprint, so only a subset of the jQuery API - methods, arguments and - * invocation styles - are supported. - * - * Note: All element references in Angular are always wrapped with jQuery or jqLite; they are never - * raw DOM references. - * - * ## Angular's jQuery lite provides the following methods: - * - * - [addClass()](http://api.jquery.com/addClass/) - * - [after()](http://api.jquery.com/after/) - * - [append()](http://api.jquery.com/append/) - * - [attr()](http://api.jquery.com/attr/) - * - [bind()](http://api.jquery.com/bind/) - Does not support namespaces - * - [children()](http://api.jquery.com/children/) - Does not support selectors - * - [clone()](http://api.jquery.com/clone/) - * - [contents()](http://api.jquery.com/contents/) - * - [css()](http://api.jquery.com/css/) - * - [data()](http://api.jquery.com/data/) - * - [eq()](http://api.jquery.com/eq/) - * - [find()](http://api.jquery.com/find/) - Limited to lookups by tag name - * - [hasClass()](http://api.jquery.com/hasClass/) - * - [html()](http://api.jquery.com/html/) - * - [next()](http://api.jquery.com/next/) - Does not support selectors - * - [parent()](http://api.jquery.com/parent/) - Does not support selectors - * - [prepend()](http://api.jquery.com/prepend/) - * - [prop()](http://api.jquery.com/prop/) - * - [ready()](http://api.jquery.com/ready/) - * - [remove()](http://api.jquery.com/remove/) - * - [removeAttr()](http://api.jquery.com/removeAttr/) - * - [removeClass()](http://api.jquery.com/removeClass/) - * - [removeData()](http://api.jquery.com/removeData/) - * - [replaceWith()](http://api.jquery.com/replaceWith/) - * - [text()](http://api.jquery.com/text/) - * - [toggleClass()](http://api.jquery.com/toggleClass/) - * - [triggerHandler()](http://api.jquery.com/triggerHandler/) - Doesn't pass native event objects to handlers. - * - [unbind()](http://api.jquery.com/unbind/) - Does not support namespaces - * - [val()](http://api.jquery.com/val/) - * - [wrap()](http://api.jquery.com/wrap/) - * - * ## In addtion to the above, Angular provides additional methods to both jQuery and jQuery lite: * + * If jQuery is available, `angular.element` is an alias for the + * [jQuery](http://api.jquery.com/jQuery/) function. If jQuery is not available, `angular.element` + * delegates to Angular's built-in subset of jQuery, called "jQuery lite" or "jqLite." + * + *
jqLite is a tiny, API-compatible subset of jQuery that allows + * Angular to manipulate the DOM in a cross-browser compatible way. **jqLite** implements only the most + * commonly needed functionality with the goal of having a very small footprint.
+ * + * To use jQuery, simply load it before `DOMContentLoaded` event fired. + * + *
**Note:** all element references in Angular are always wrapped with jQuery or + * jqLite; they are never raw DOM references.
+ * + * ## Angular's jqLite + * jqLite provides only the following jQuery methods: + * + * - [`addClass()`](http://api.jquery.com/addClass/) + * - [`after()`](http://api.jquery.com/after/) + * - [`append()`](http://api.jquery.com/append/) + * - [`attr()`](http://api.jquery.com/attr/) + * - [`bind()`](http://api.jquery.com/bind/) - Does not support namespaces, selectors or eventData + * - [`children()`](http://api.jquery.com/children/) - Does not support selectors + * - [`clone()`](http://api.jquery.com/clone/) + * - [`contents()`](http://api.jquery.com/contents/) + * - [`css()`](http://api.jquery.com/css/) - Only retrieves inline-styles, does not call `getComputedStyles()` + * - [`data()`](http://api.jquery.com/data/) + * - [`empty()`](http://api.jquery.com/empty/) + * - [`eq()`](http://api.jquery.com/eq/) + * - [`find()`](http://api.jquery.com/find/) - Limited to lookups by tag name + * - [`hasClass()`](http://api.jquery.com/hasClass/) + * - [`html()`](http://api.jquery.com/html/) + * - [`next()`](http://api.jquery.com/next/) - Does not support selectors + * - [`on()`](http://api.jquery.com/on/) - Does not support namespaces, selectors or eventData + * - [`off()`](http://api.jquery.com/off/) - Does not support namespaces or selectors + * - [`one()`](http://api.jquery.com/one/) - Does not support namespaces or selectors + * - [`parent()`](http://api.jquery.com/parent/) - Does not support selectors + * - [`prepend()`](http://api.jquery.com/prepend/) + * - [`prop()`](http://api.jquery.com/prop/) + * - [`ready()`](http://api.jquery.com/ready/) + * - [`remove()`](http://api.jquery.com/remove/) + * - [`removeAttr()`](http://api.jquery.com/removeAttr/) + * - [`removeClass()`](http://api.jquery.com/removeClass/) + * - [`removeData()`](http://api.jquery.com/removeData/) + * - [`replaceWith()`](http://api.jquery.com/replaceWith/) + * - [`text()`](http://api.jquery.com/text/) + * - [`toggleClass()`](http://api.jquery.com/toggleClass/) + * - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers. + * - [`unbind()`](http://api.jquery.com/unbind/) - Does not support namespaces + * - [`val()`](http://api.jquery.com/val/) + * - [`wrap()`](http://api.jquery.com/wrap/) + * + * ## jQuery/jqLite Extras + * Angular also provides the following additional methods and events to both jQuery and jqLite: + * + * ### Events + * - `$destroy` - AngularJS intercepts all jqLite/jQuery's DOM destruction apis and fires this event + * on all DOM nodes being removed. This can be used to clean up any 3rd party bindings to the DOM + * element before it is removed. + * + * ### Methods * - `controller(name)` - retrieves the controller of the current element or its parent. By default * retrieves controller associated with the `ngController` directive. If `name` is provided as * camelCase directive name, then the controller for this directive will be retrieved (e.g. * `'ngModel'`). * - `injector()` - retrieves the injector of the current element or its parent. - * - `scope()` - retrieves the {@link api/ng.$rootScope.Scope scope} of the current + * - `scope()` - retrieves the {@link ng.$rootScope.Scope scope} of the current * element or its parent. + * - `isolateScope()` - retrieves an isolate {@link ng.$rootScope.Scope scope} if one is attached directly to the + * current element. This getter should be used only on elements that contain a directive which starts a new isolate + * scope. Calling `scope()` on this element always returns the original non-isolate scope. * - `inheritedData()` - same as `data()`, but walks up the DOM until a value is found or the top * parent element is reached. * @@ -1505,8 +2211,9 @@ function publishExternalAPI(angular){ * @returns {Object} jQuery object. */ +JQLite.expando = 'ng339'; + var jqCache = JQLite.cache = {}, - jqName = JQLite.expando = 'ng-' + new Date().getTime(), jqId = 1, addEventListenerFn = (window.document.addEventListener ? function(element, type, fn) {element.addEventListener(type, fn, false);} @@ -1515,11 +2222,20 @@ var jqCache = JQLite.cache = {}, ? function(element, type, fn) {element.removeEventListener(type, fn, false); } : function(element, type, fn) {element.detachEvent('on' + type, fn); }); +/* + * !!! This is an undocumented "private" function !!! + */ +var jqData = JQLite._data = function(node) { + //jQuery always returns an object on cache miss + return this.cache[node[this.expando]] || {}; +}; + function jqNextId() { return ++jqId; } var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; var MOZ_HACK_REGEXP = /^moz([A-Z])/; +var jqLiteMinErr = minErr('jqLite'); /** * Converts snake_case to camelCase. @@ -1537,37 +2253,39 @@ function camelCase(name) { ///////////////////////////////////////////// // jQuery mutation patch // -// In conjunction with bindJQuery intercepts all jQuery's DOM destruction apis and fires a +// In conjunction with bindJQuery intercepts all jQuery's DOM destruction apis and fires a // $destroy event on all DOM nodes being removed. // ///////////////////////////////////////////// -function JQLitePatchJQueryRemove(name, dispatchThis) { +function jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) { var originalJqFn = jQuery.fn[name]; originalJqFn = originalJqFn.$original || originalJqFn; removePatch.$original = originalJqFn; jQuery.fn[name] = removePatch; - function removePatch() { - var list = [this], + function removePatch(param) { + // jshint -W040 + var list = filterElems && param ? [this.filter(param)] : [this], fireEvent = dispatchThis, set, setIndex, setLength, - element, childIndex, childLength, children, - fns, events; - - while(list.length) { - set = list.shift(); - for(setIndex = 0, setLength = set.length; setIndex < setLength; setIndex++) { - element = jqLite(set[setIndex]); - if (fireEvent) { - element.triggerHandler('$destroy'); - } else { - fireEvent = !fireEvent; - } - for(childIndex = 0, childLength = (children = element.children()).length; - childIndex < childLength; - childIndex++) { - list.push(jQuery(children[childIndex])); + element, childIndex, childLength, children; + + if (!getterIfNoArguments || param != null) { + while(list.length) { + set = list.shift(); + for(setIndex = 0, setLength = set.length; setIndex < setLength; setIndex++) { + element = jqLite(set[setIndex]); + if (fireEvent) { + element.triggerHandler('$destroy'); + } else { + fireEvent = !fireEvent; + } + for(childIndex = 0, childLength = (children = element.children()).length; + childIndex < childLength; + childIndex++) { + list.push(jQuery(children[childIndex])); + } } } } @@ -1575,45 +2293,115 @@ function JQLitePatchJQueryRemove(name, dispatchThis) { } } +var SINGLE_TAG_REGEXP = /^<(\w+)\s*\/?>(?:<\/\1>|)$/; +var HTML_REGEXP = /<|&#?\w+;/; +var TAG_NAME_REGEXP = /<([\w:]+)/; +var XHTML_TAG_REGEXP = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi; + +var wrapMap = { + 'option': [1, ''], + + 'thead': [1, '', '
'], + 'col': [2, '', '
'], + 'tr': [2, '', '
'], + 'td': [3, '', '
'], + '_default': [0, "", ""] +}; + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +function jqLiteIsTextNode(html) { + return !HTML_REGEXP.test(html); +} + +function jqLiteBuildFragment(html, context) { + var elem, tmp, tag, wrap, + fragment = context.createDocumentFragment(), + nodes = [], i, j, jj; + + if (jqLiteIsTextNode(html)) { + // Convert non-html into a text node + nodes.push(context.createTextNode(html)); + } else { + tmp = fragment.appendChild(context.createElement('div')); + // Convert html into DOM nodes + tag = (TAG_NAME_REGEXP.exec(html) || ["", ""])[1].toLowerCase(); + wrap = wrapMap[tag] || wrapMap._default; + tmp.innerHTML = '
 
' + + wrap[1] + html.replace(XHTML_TAG_REGEXP, "<$1>") + wrap[2]; + tmp.removeChild(tmp.firstChild); + + // Descend through wrappers to the right content + i = wrap[0]; + while (i--) { + tmp = tmp.lastChild; + } + + for (j=0, jj=tmp.childNodes.length; j -1); } -function JQLiteRemoveClass(element, cssClasses) { - if (cssClasses) { +function jqLiteRemoveClass(element, cssClasses) { + if (cssClasses && element.setAttribute) { forEach(cssClasses.split(' '), function(cssClass) { - element.className = trim( - (" " + element.className + " ") + element.setAttribute('class', trim( + (" " + (element.getAttribute('class') || '') + " ") .replace(/[\n\t]/g, " ") - .replace(" " + trim(cssClass) + " ", " ") + .replace(" " + trim(cssClass) + " ", " ")) ); }); } } -function JQLiteAddClass(element, cssClasses) { - if (cssClasses) { +function jqLiteAddClass(element, cssClasses) { + if (cssClasses && element.setAttribute) { + var existingClasses = (' ' + (element.getAttribute('class') || '') + ' ') + .replace(/[\n\t]/g, " "); + forEach(cssClasses.split(' '), function(cssClass) { - if (!JQLiteHasClass(element, cssClass)) { - element.className = trim(element.className + ' ' + trim(cssClass)); + cssClass = trim(cssClass); + if (existingClasses.indexOf(' ' + cssClass + ' ') === -1) { + existingClasses += cssClass + ' '; } }); + + element.setAttribute('class', trim(existingClasses)); } } -function JQLiteAddNodes(root, elements) { +function jqLiteAddNodes(root, elements) { if (elements) { elements = (!elements.nodeName && isDefined(elements.length) && !isWindow(elements)) ? elements @@ -1725,22 +2527,36 @@ function JQLiteAddNodes(root, elements) { } } -function JQLiteController(element, name) { - return JQLiteInheritedData(element, '$' + (name || 'ngController' ) + 'Controller'); +function jqLiteController(element, name) { + return jqLiteInheritedData(element, '$' + (name || 'ngController' ) + 'Controller'); } -function JQLiteInheritedData(element, name, value) { - element = jqLite(element); - +function jqLiteInheritedData(element, name, value) { // if element is the document object work with the html element instead // this makes $(document).scope() possible - if(element[0].nodeType == 9) { - element = element.find('html'); + if(element.nodeType == 9) { + element = element.documentElement; } + var names = isArray(name) ? name : [name]; - while (element.length) { - if (value = element.data(name)) return value; - element = element.parent(); + while (element) { + for (var i = 0, ii = names.length; i < ii; i++) { + if ((value = jqLite.data(element, names[i])) !== undefined) return value; + } + + // If dealing with a document fragment node with a host element, and no parent, use the host + // element as the parent. This enables directives within a Shadow DOM or polyfilled Shadow DOM + // to lookup parent controllers. + element = element.parentNode || (element.nodeType === 11 && element.host); + } +} + +function jqLiteEmpty(element) { + for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) { + jqLiteDealoc(childNodes[i]); + } + while (element.firstChild) { + element.removeChild(element.firstChild); } } @@ -1757,9 +2573,16 @@ var JQLitePrototype = JQLite.prototype = { fn(); } - this.bind('DOMContentLoaded', trigger); // works for modern browsers and IE9 - // we can not use jqLite since we are not done loading and jQuery could be loaded later. - JQLite(window).bind('load', trigger); // fallback to window.onload for others + // check if document already is loaded + if (document.readyState === 'complete'){ + setTimeout(trigger); + } else { + this.on('DOMContentLoaded', trigger); // works for modern browsers and IE9 + // we can not use jqLite since we are not done loading and jQuery could be loaded later. + // jshint -W064 + JQLite(window).on('load', trigger); // fallback to window.onload for others + // jshint +W064 + } }, toString: function() { var value = []; @@ -1783,11 +2606,11 @@ var JQLitePrototype = JQLite.prototype = { // value on get. ////////////////////////////////////////// var BOOLEAN_ATTR = {}; -forEach('multiple,selected,checked,disabled,readOnly,required'.split(','), function(value) { +forEach('multiple,selected,checked,disabled,readOnly,required,open'.split(','), function(value) { BOOLEAN_ATTR[lowercase(value)] = value; }); var BOOLEAN_ELEMENTS = {}; -forEach('input,select,option,textarea,button,form'.split(','), function(value) { +forEach('input,select,option,textarea,button,form,details'.split(','), function(value) { BOOLEAN_ELEMENTS[uppercase(value)] = true; }); @@ -1800,24 +2623,37 @@ function getBooleanAttrName(element, name) { } forEach({ - data: JQLiteData, - inheritedData: JQLiteInheritedData, + data: jqLiteData, + removeData: jqLiteRemoveData +}, function(fn, name) { + JQLite[name] = fn; +}); + +forEach({ + data: jqLiteData, + inheritedData: jqLiteInheritedData, scope: function(element) { - return JQLiteInheritedData(element, '$scope'); + // Can't use jqLiteData here directly so we stay compatible with jQuery! + return jqLite.data(element, '$scope') || jqLiteInheritedData(element.parentNode || element, ['$isolateScope', '$scope']); + }, + + isolateScope: function(element) { + // Can't use jqLiteData here directly so we stay compatible with jQuery! + return jqLite.data(element, '$isolateScope') || jqLite.data(element, '$isolateScopeNoTemplate'); }, - controller: JQLiteController , + controller: jqLiteController, injector: function(element) { - return JQLiteInheritedData(element, '$injector'); + return jqLiteInheritedData(element, '$injector'); }, removeAttr: function(element,name) { element.removeAttribute(name); }, - hasClass: JQLiteHasClass, + hasClass: jqLiteHasClass, css: function(element, name, value) { name = camelCase(name); @@ -1880,27 +2716,38 @@ forEach({ } }, - text: extend((msie < 9) - ? function(element, value) { - if (element.nodeType == 1 /** Element */) { - if (isUndefined(value)) - return element.innerText; - element.innerText = value; - } else { - if (isUndefined(value)) - return element.nodeValue; - element.nodeValue = value; - } - } - : function(element, value) { - if (isUndefined(value)) { - return element.textContent; - } - element.textContent = value; - }, {$dv:''}), + text: (function() { + var NODE_TYPE_TEXT_PROPERTY = []; + if (msie < 9) { + NODE_TYPE_TEXT_PROPERTY[1] = 'innerText'; /** Element **/ + NODE_TYPE_TEXT_PROPERTY[3] = 'nodeValue'; /** Text **/ + } else { + NODE_TYPE_TEXT_PROPERTY[1] = /** Element **/ + NODE_TYPE_TEXT_PROPERTY[3] = 'textContent'; /** Text **/ + } + getText.$dv = ''; + return getText; + + function getText(element, value) { + var textProp = NODE_TYPE_TEXT_PROPERTY[element.nodeType]; + if (isUndefined(value)) { + return textProp ? element[textProp] : ''; + } + element[textProp] = value; + } + })(), val: function(element, value) { if (isUndefined(value)) { + if (nodeName_(element) === 'SELECT' && element.multiple) { + var result = []; + forEach(element.options, function (option) { + if (option.selected) { + result.push(option.value || option.text); + } + }); + return result.length === 0 ? null : result; + } return element.value; } element.value = value; @@ -1911,25 +2758,30 @@ forEach({ return element.innerHTML; } for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) { - JQLiteDealoc(childNodes[i]); + jqLiteDealoc(childNodes[i]); } element.innerHTML = value; - } + }, + + empty: jqLiteEmpty }, function(fn, name){ /** * Properties: writes return selection, reads return first value */ JQLite.prototype[name] = function(arg1, arg2) { var i, key; + var nodeCount = this.length; - // JQLiteHasClass has only two arguments, but is a getter-only fn, so we need to special-case it + // jqLiteHasClass has only two arguments, but is a getter-only fn, so we need to special-case it // in a way that survives minification. - if (((fn.length == 2 && (fn !== JQLiteHasClass && fn !== JQLiteController)) ? arg1 : arg2) === undefined) { + // jqLiteEmpty takes no arguments but is a setter. + if (fn !== jqLiteEmpty && + (((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2) === undefined)) { if (isObject(arg1)) { // we are a write, but the object properties are the key/values - for(i=0; i < this.length; i++) { - if (fn === JQLiteData) { + for (i = 0; i < nodeCount; i++) { + if (fn === jqLiteData) { // data() takes the whole object in jQuery fn(this[i], arg1); } else { @@ -1942,18 +2794,24 @@ forEach({ return this; } else { // we are a read, so read the first child. - if (this.length) - return fn(this[0], arg1, arg2); + // TODO: do we still need this? + var value = fn.$dv; + // Only if we have $dv do we iterate over all, otherwise it is just the first element. + var jj = (value === undefined) ? Math.min(nodeCount, 1) : nodeCount; + for (var j = 0; j < jj; j++) { + var nodeValue = fn(this[j], arg1, arg2); + value = value ? value + nodeValue : nodeValue; + } + return value; } } else { // we are a write, so apply to all children - for(i=0; i < this.length; i++) { + for (i = 0; i < nodeCount; i++) { fn(this[i], arg1, arg2); } // return self for chaining return this; } - return fn.$dv; }; }); @@ -1985,10 +2843,13 @@ function createEventHandler(element, events) { } event.isDefaultPrevented = function() { - return event.defaultPrevented; + return event.defaultPrevented || event.returnValue === false; }; - forEach(events[type || event.type], function(fn) { + // Copy event handlers in case event handlers array is modified during execution. + var eventHandlersCopy = shallowCopy(events[type || event.type] || []); + + forEach(eventHandlersCopy, function(fn) { fn.call(element, event); }); @@ -2016,16 +2877,18 @@ function createEventHandler(element, events) { // selector. ////////////////////////////////////////// forEach({ - removeData: JQLiteRemoveData, + removeData: jqLiteRemoveData, + + dealoc: jqLiteDealoc, - dealoc: JQLiteDealoc, + on: function onFn(element, type, fn, unsupported){ + if (isDefined(unsupported)) throw jqLiteMinErr('onargs', 'jqLite#on() does not support the `selector` or `eventData` parameters'); - bind: function bindFn(element, type, fn){ - var events = JQLiteExpandoStore(element, 'events'), - handle = JQLiteExpandoStore(element, 'handle'); + var events = jqLiteExpandoStore(element, 'events'), + handle = jqLiteExpandoStore(element, 'handle'); - if (!events) JQLiteExpandoStore(element, 'events', events = {}); - if (!handle) JQLiteExpandoStore(element, 'handle', handle = createEventHandler(element, events)); + if (!events) jqLiteExpandoStore(element, 'events', events = {}); + if (!handle) jqLiteExpandoStore(element, 'handle', handle = createEventHandler(element, events)); forEach(type.split(' '), function(type){ var eventFns = events[type]; @@ -2034,6 +2897,7 @@ forEach({ if (type == 'mouseenter' || type == 'mouseleave') { var contains = document.body.contains || document.body.compareDocumentPosition ? function( a, b ) { + // jshint bitwise: false var adown = a.nodeType === 9 ? a.documentElement : a, bup = b && b.parentNode; return a === bup || !!( bup && bup.nodeType === 1 && ( @@ -2051,39 +2915,52 @@ forEach({ } } return false; - }; + }; events[type] = []; - - // Refer to jQuery's implementation of mouseenter & mouseleave + + // Refer to jQuery's implementation of mouseenter & mouseleave // Read about mouseenter and mouseleave: // http://www.quirksmode.org/js/events_mouse.html#link8 - var eventmap = { mouseleave : "mouseout", mouseenter : "mouseover"} - bindFn(element, eventmap[type], function(event) { - var ret, target = this, related = event.relatedTarget; + var eventmap = { mouseleave : "mouseout", mouseenter : "mouseover"}; + + onFn(element, eventmap[type], function(event) { + var target = this, related = event.relatedTarget; // For mousenter/leave call the handler if related is outside the target. // NB: No relatedTarget if the mouse left/entered the browser window if ( !related || (related !== target && !contains(target, related)) ){ handle(event, type); - } - + } }); } else { addEventListenerFn(element, type, handle); events[type] = []; } - eventFns = events[type] + eventFns = events[type]; } eventFns.push(fn); }); }, - unbind: JQLiteUnbind, + off: jqLiteOff, + + one: function(element, type, fn) { + element = jqLite(element); + + //add the listener twice so that when it is called + //you can remove the original function and still be + //able to call element.off(ev, fn) normally + element.on(type, function onFn() { + element.off(type, fn); + element.off(type, onFn); + }); + element.on(type, fn); + }, replaceWith: function(element, replaceNode) { var index, parent = element.parentNode; - JQLiteDealoc(element); + jqLiteDealoc(element); forEach(new JQLite(replaceNode), function(node){ if (index) { parent.insertBefore(node, index.nextSibling); @@ -2104,13 +2981,14 @@ forEach({ }, contents: function(element) { - return element.childNodes || []; + return element.contentDocument || element.childNodes || []; }, append: function(element, node) { forEach(new JQLite(node), function(child){ - if (element.nodeType === 1) + if (element.nodeType === 1 || element.nodeType === 11) { element.appendChild(child); + } }); }, @@ -2118,12 +2996,7 @@ forEach({ if (element.nodeType === 1) { var index = element.firstChild; forEach(new JQLite(node), function(child){ - if (index) { - element.insertBefore(child, index); - } else { - element.appendChild(child); - index = child; - } + element.insertBefore(child, index); }); } }, @@ -2138,7 +3011,7 @@ forEach({ }, remove: function(element) { - JQLiteDealoc(element); + jqLiteDealoc(element); var parent = element.parentNode; if (parent) parent.removeChild(element); }, @@ -2151,14 +3024,19 @@ forEach({ }); }, - addClass: JQLiteAddClass, - removeClass: JQLiteRemoveClass, + addClass: jqLiteAddClass, + removeClass: jqLiteRemoveClass, toggleClass: function(element, selector, condition) { - if (isUndefined(condition)) { - condition = !JQLiteHasClass(element, selector); + if (selector) { + forEach(selector.split(' '), function(className){ + var classCondition = condition; + if (isUndefined(classCondition)) { + classCondition = !jqLiteHasClass(element, className); + } + (classCondition ? jqLiteAddClass : jqLiteRemoveClass)(element, className); + }); } - (condition ? JQLiteAddClass : JQLiteRemoveClass)(element, selector); }, parent: function(element) { @@ -2180,37 +3058,70 @@ forEach({ }, find: function(element, selector) { - return element.getElementsByTagName(selector); + if (element.getElementsByTagName) { + return element.getElementsByTagName(selector); + } else { + return []; + } }, - clone: JQLiteClone, + clone: jqLiteClone, - triggerHandler: function(element, eventName) { - var eventFns = (JQLiteExpandoStore(element, 'events') || {})[eventName]; + triggerHandler: function(element, event, extraParameters) { - forEach(eventFns, function(fn) { - fn.call(element, null); - }); + var dummyEvent, eventFnsCopy, handlerArgs; + var eventName = event.type || event; + var eventFns = (jqLiteExpandoStore(element, 'events') || {})[eventName]; + + if (eventFns) { + + // Create a dummy event to pass to the handlers + dummyEvent = { + preventDefault: function() { this.defaultPrevented = true; }, + isDefaultPrevented: function() { return this.defaultPrevented === true; }, + stopPropagation: noop, + type: eventName, + target: element + }; + + // If a custom event was provided then extend our dummy event with it + if (event.type) { + dummyEvent = extend(dummyEvent, event); + } + + // Copy event handlers in case event handlers array is modified during execution. + eventFnsCopy = shallowCopy(eventFns); + handlerArgs = extraParameters ? [dummyEvent].concat(extraParameters) : [dummyEvent]; + + forEach(eventFnsCopy, function(fn) { + fn.apply(element, handlerArgs); + }); + + } } }, function(fn, name){ /** * chaining functions */ - JQLite.prototype[name] = function(arg1, arg2) { + JQLite.prototype[name] = function(arg1, arg2, arg3) { var value; for(var i=0; i < this.length; i++) { - if (value == undefined) { - value = fn(this[i], arg1, arg2); - if (value !== undefined) { + if (isUndefined(value)) { + value = fn(this[i], arg1, arg2, arg3); + if (isDefined(value)) { // any function which returns a value needs to be wrapped value = jqLite(value); } } else { - JQLiteAddNodes(value, fn(this[i], arg1, arg2)); + jqLiteAddNodes(value, fn(this[i], arg1, arg2, arg3)); } } - return value == undefined ? this : value; + return isDefined(value) ? value : this; }; + + // bind legacy bind/unbind to on/off + JQLite.prototype.bind = JQLite.prototype.on; + JQLite.prototype.unbind = JQLite.prototype.off; }); /** @@ -2225,16 +3136,16 @@ forEach({ * @returns {string} hash string such that the same input will have the same hash string. * The resulting string key is in 'type:hashKey' format. */ -function hashKey(obj) { +function hashKey(obj, nextUidFn) { var objType = typeof obj, key; - if (objType == 'object' && obj !== null) { + if (objType == 'function' || (objType == 'object' && obj !== null)) { if (typeof (key = obj.$$hashKey) == 'function') { // must invoke on object to keep the right this key = obj.$$hashKey(); } else if (key === undefined) { - key = obj.$$hashKey = nextUid(); + key = obj.$$hashKey = (nextUidFn || nextUid)(); } } else { key = obj; @@ -2246,7 +3157,13 @@ function hashKey(obj) { /** * HashMap which can use objects as keys */ -function HashMap(array){ +function HashMap(array, isolatedUid) { + if (isolatedUid) { + var uid = 0; + this.nextUid = function() { + return ++uid; + }; + } forEach(array, this.put, this); } HashMap.prototype = { @@ -2256,15 +3173,15 @@ HashMap.prototype = { * @param value value to store can be any type */ put: function(key, value) { - this[hashKey(key)] = value; + this[hashKey(key, this.nextUid)] = value; }, /** * @param key - * @returns the value for the key + * @returns {Object} the value for the key */ get: function(key) { - return this[hashKey(key)]; + return this[hashKey(key, this.nextUid)]; }, /** @@ -2272,73 +3189,30 @@ HashMap.prototype = { * @param key */ remove: function(key) { - var value = this[key = hashKey(key)]; + var value = this[key = hashKey(key, this.nextUid)]; delete this[key]; return value; } }; -/** - * A map where multiple values can be added to the same key such that they form a queue. - * @returns {HashQueueMap} - */ -function HashQueueMap() {} -HashQueueMap.prototype = { - /** - * Same as array push, but using an array as the value for the hash - */ - push: function(key, value) { - var array = this[key = hashKey(key)]; - if (!array) { - this[key] = [value]; - } else { - array.push(value); - } - }, - - /** - * Same as array shift, but using an array as the value for the hash - */ - shift: function(key) { - var array = this[key = hashKey(key)]; - if (array) { - if (array.length == 1) { - delete this[key]; - return array[0]; - } else { - return array.shift(); - } - } - }, - - /** - * return the first item without deleting it - */ - peek: function(key) { - var array = this[hashKey(key)]; - if (array) { - return array[0]; - } - } -}; - /** * @ngdoc function + * @module ng * @name angular.injector - * @function + * @kind function * * @description - * Creates an injector function that can be used for retrieving services as well as for + * Creates an injector object that can be used for retrieving services as well as for * dependency injection (see {@link guide/di dependency injection}). * * @param {Array.} modules A list of module functions or their aliases. See * {@link angular.module}. The `ng` module must be explicitly added. - * @returns {function()} Injector function. See {@link AUTO.$injector $injector}. + * @returns {injector} Injector object. See {@link auto.$injector $injector}. * * @example * Typical usage - *
+ * ```js
  *   // create an injector
  *   var $injector = angular.injector(['ng']);
  *
@@ -2348,38 +3222,63 @@ HashQueueMap.prototype = {
  *     $compile($document)($rootScope);
  *     $rootScope.$digest();
  *   });
- * 
+ * ``` + * + * Sometimes you want to get access to the injector of a currently running Angular app + * from outside Angular. Perhaps, you want to inject and compile some markup after the + * application has been bootstrapped. You can do this using the extra `injector()` added + * to JQuery/jqLite elements. See {@link angular.element}. + * + * *This is fairly rare but could be the case if a third party library is injecting the + * markup.* + * + * In the following example a new block of HTML containing a `ng-controller` + * directive is added to the end of the document body by JQuery. We then compile and link + * it into the current AngularJS scope. + * + * ```js + * var $div = $('
{{content.label}}
'); + * $(document.body).append($div); + * + * angular.element(document).injector().invoke(function($compile) { + * var scope = angular.element($div).scope(); + * $compile($div)(scope); + * }); + * ``` */ /** - * @ngdoc overview - * @name AUTO + * @ngdoc module + * @name auto * @description * - * Implicit module which gets automatically added to each {@link AUTO.$injector $injector}. + * Implicit module which gets automatically added to each {@link auto.$injector $injector}. */ var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; var FN_ARG_SPLIT = /,/; var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; +var $injectorMinErr = minErr('$injector'); function annotate(fn) { var $inject, fnText, argDecl, last; - if (typeof fn == 'function') { + if (typeof fn === 'function') { if (!($inject = fn.$inject)) { $inject = []; - fnText = fn.toString().replace(STRIP_COMMENTS, ''); - argDecl = fnText.match(FN_ARGS); - forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){ - arg.replace(FN_ARG, function(all, underscore, name){ - $inject.push(name); + if (fn.length) { + fnText = fn.toString().replace(STRIP_COMMENTS, ''); + argDecl = fnText.match(FN_ARGS); + forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){ + arg.replace(FN_ARG, function(all, underscore, name){ + $inject.push(name); + }); }); - }); + } fn.$inject = $inject; } } else if (isArray(fn)) { @@ -2395,32 +3294,31 @@ function annotate(fn) { /////////////////////////////////////// /** - * @ngdoc object - * @name AUTO.$injector - * @function + * @ngdoc service + * @name $injector * * @description * * `$injector` is used to retrieve object instances as defined by - * {@link AUTO.$provide provider}, instantiate types, invoke methods, + * {@link auto.$provide provider}, instantiate types, invoke methods, * and load modules. * * The following always holds true: * - *
+ * ```js
  *   var $injector = angular.injector();
  *   expect($injector.get('$injector')).toBe($injector);
  *   expect($injector.invoke(function($injector){
  *     return $injector;
- *   }).toBe($injector);
- * 
+ * })).toBe($injector); + * ``` * * # Injection Function Annotation * * JavaScript does not have annotations, and annotations are needed for dependency injection. The * following are all valid ways of annotating function with injection arguments and are equivalent. * - *
+ * ```js
  *   // inferred (only works if code not minified/obfuscated)
  *   $injector.invoke(function(serviceA){});
  *
@@ -2431,16 +3329,16 @@ function annotate(fn) {
  *
  *   // inline
  *   $injector.invoke(['serviceA', function(serviceA){}]);
- * 
+ * ``` * * ## Inference * - * In JavaScript calling `toString()` on a function returns the function definition. The definition can then be - * parsed and the function arguments can be extracted. *NOTE:* This does not work with minification, and obfuscation - * tools since these tools change the argument names. + * In JavaScript calling `toString()` on a function returns the function definition. The definition + * can then be parsed and the function arguments can be extracted. *NOTE:* This does not work with + * minification, and obfuscation tools since these tools change the argument names. * * ## `$inject` Annotation - * By adding a `$inject` property onto a function the injection parameters can be specified. + * By adding an `$inject` property onto a function the injection parameters can be specified. * * ## Inline * As an array of injection names, where the last item in the array is the function to call. @@ -2448,8 +3346,7 @@ function annotate(fn) { /** * @ngdoc method - * @name AUTO.$injector#get - * @methodOf AUTO.$injector + * @name $injector#get * * @description * Return an instance of the service. @@ -2460,48 +3357,60 @@ function annotate(fn) { /** * @ngdoc method - * @name AUTO.$injector#invoke - * @methodOf AUTO.$injector + * @name $injector#invoke * * @description * Invoke the method and supply the method arguments from the `$injector`. * - * @param {!function} fn The function to invoke. The function arguments come form the function annotation. + * @param {!Function} fn The function to invoke. Function parameters are injected according to the + * {@link guide/di $inject Annotation} rules. * @param {Object=} self The `this` for the invoked method. - * @param {Object=} locals Optional object. If preset then any argument names are read from this object first, before - * the `$injector` is consulted. + * @param {Object=} locals Optional object. If preset then any argument names are read from this + * object first, before the `$injector` is consulted. * @returns {*} the value returned by the invoked `fn` function. */ /** * @ngdoc method - * @name AUTO.$injector#instantiate - * @methodOf AUTO.$injector + * @name $injector#has + * + * @description + * Allows the user to query if the particular service exists. + * + * @param {string} name Name of the service to query. + * @returns {boolean} `true` if injector has given service. + */ + +/** + * @ngdoc method + * @name $injector#instantiate * @description - * Create a new instance of JS type. The method takes a constructor function invokes the new operator and supplies - * all of the arguments to the constructor function as specified by the constructor annotation. + * Create a new instance of JS type. The method takes a constructor function, invokes the new + * operator, and supplies all of the arguments to the constructor function as specified by the + * constructor annotation. * - * @param {function} Type Annotated constructor function. - * @param {Object=} locals Optional object. If preset then any argument names are read from this object first, before - * the `$injector` is consulted. + * @param {Function} Type Annotated constructor function. + * @param {Object=} locals Optional object. If preset then any argument names are read from this + * object first, before the `$injector` is consulted. * @returns {Object} new instance of `Type`. */ /** * @ngdoc method - * @name AUTO.$injector#annotate - * @methodOf AUTO.$injector + * @name $injector#annotate * * @description - * Returns an array of service names which the function is requesting for injection. This API is used by the injector - * to determine which services need to be injected into the function when the function is invoked. There are three - * ways in which the function can be annotated with the needed dependencies. + * Returns an array of service names which the function is requesting for injection. This API is + * used by the injector to determine which services need to be injected into the function when the + * function is invoked. There are three ways in which the function can be annotated with the needed + * dependencies. * * # Argument names * - * The simplest form is to extract the dependencies from the arguments of the function. This is done by converting - * the function into a string using `toString()` method and extracting the argument names. - *
+ * The simplest form is to extract the dependencies from the arguments of the function. This is done
+ * by converting the function into a string using `toString()` method and extracting the argument
+ * names.
+ * ```js
  *   // Given
  *   function MyController($scope, $route) {
  *     // ...
@@ -2509,34 +3418,34 @@ function annotate(fn) {
  *
  *   // Then
  *   expect(injector.annotate(MyController)).toEqual(['$scope', '$route']);
- * 
+ * ``` * - * This method does not work with code minfication / obfuscation. For this reason the following annotation strategies - * are supported. + * This method does not work with code minification / obfuscation. For this reason the following + * annotation strategies are supported. * * # The `$inject` property * - * If a function has an `$inject` property and its value is an array of strings, then the strings represent names of - * services to be injected into the function. - *
+ * If a function has an `$inject` property and its value is an array of strings, then the strings
+ * represent names of services to be injected into the function.
+ * ```js
  *   // Given
  *   var MyController = function(obfuscatedScope, obfuscatedRoute) {
  *     // ...
  *   }
  *   // Define function dependencies
- *   MyController.$inject = ['$scope', '$route'];
+ *   MyController['$inject'] = ['$scope', '$route'];
  *
  *   // Then
  *   expect(injector.annotate(MyController)).toEqual(['$scope', '$route']);
- * 
+ * ``` * * # The array notation * - * It is often desirable to inline Injected functions and that's when setting the `$inject` property is very - * inconvenient. In these situations using the array notation to specify the dependencies in a way that survives - * minification is a better choice: + * It is often desirable to inline Injected functions and that's when setting the `$inject` property + * is very inconvenient. In these situations using the array notation to specify the dependencies in + * a way that survives minification is a better choice: * - *
+ * ```js
  *   // We wish to write this (not minification / obfuscation safe)
  *   injector.invoke(function($compile, $rootScope) {
  *     // ...
@@ -2558,10 +3467,10 @@ function annotate(fn) {
  *   expect(injector.annotate(
  *      ['$compile', '$rootScope', function(obfus_$compile, obfus_$rootScope) {}])
  *    ).toEqual(['$compile', '$rootScope']);
- * 
+ * ``` * - * @param {function|Array.} fn Function for which dependent service names need to be retrieved as described - * above. + * @param {Function|Array.} fn Function for which dependent service names need to + * be retrieved as described above. * * @returns {Array.} The names of the services which the function requires. */ @@ -2570,148 +3479,305 @@ function annotate(fn) { /** - * @ngdoc object - * @name AUTO.$provide + * @ngdoc service + * @name $provide * * @description * - * Use `$provide` to register new providers with the `$injector`. The providers are the factories for the instance. - * The providers share the same name as the instance they create with `Provider` suffixed to them. - * - * A provider is an object with a `$get()` method. The injector calls the `$get` method to create a new instance of - * a service. The Provider can have additional methods which would allow for configuration of the provider. - * - *
- *   function GreetProvider() {
- *     var salutation = 'Hello';
- *
- *     this.salutation = function(text) {
- *       salutation = text;
- *     };
- *
- *     this.$get = function() {
- *       return function (name) {
- *         return salutation + ' ' + name + '!';
- *       };
- *     };
- *   }
- *
- *   describe('Greeter', function(){
- *
- *     beforeEach(module(function($provide) {
- *       $provide.provider('greet', GreetProvider);
- *     }));
- *
- *     it('should greet', inject(function(greet) {
- *       expect(greet('angular')).toEqual('Hello angular!');
- *     }));
- *
- *     it('should allow configuration of salutation', function() {
- *       module(function(greetProvider) {
- *         greetProvider.salutation('Ahoj');
- *       });
- *       inject(function(greet) {
- *         expect(greet('angular')).toEqual('Ahoj angular!');
- *       });
- *     });
- * 
+ * The {@link auto.$provide $provide} service has a number of methods for registering components + * with the {@link auto.$injector $injector}. Many of these functions are also exposed on + * {@link angular.Module}. + * + * An Angular **service** is a singleton object created by a **service factory**. These **service + * factories** are functions which, in turn, are created by a **service provider**. + * The **service providers** are constructor functions. When instantiated they must contain a + * property called `$get`, which holds the **service factory** function. + * + * When you request a service, the {@link auto.$injector $injector} is responsible for finding the + * correct **service provider**, instantiating it and then calling its `$get` **service factory** + * function to get the instance of the **service**. + * + * Often services have no configuration options and there is no need to add methods to the service + * provider. The provider will be no more than a constructor function with a `$get` property. For + * these cases the {@link auto.$provide $provide} service has additional helper methods to register + * services without specifying a provider. + * + * * {@link auto.$provide#provider provider(provider)} - registers a **service provider** with the + * {@link auto.$injector $injector} + * * {@link auto.$provide#constant constant(obj)} - registers a value/object that can be accessed by + * providers and services. + * * {@link auto.$provide#value value(obj)} - registers a value/object that can only be accessed by + * services, not providers. + * * {@link auto.$provide#factory factory(fn)} - registers a service **factory function**, `fn`, + * that will be wrapped in a **service provider** object, whose `$get` property will contain the + * given factory function. + * * {@link auto.$provide#service service(class)} - registers a **constructor function**, `class` + * that will be wrapped in a **service provider** object, whose `$get` property will instantiate + * a new object using the given constructor function. + * + * See the individual methods for more information and examples. */ /** * @ngdoc method - * @name AUTO.$provide#provider - * @methodOf AUTO.$provide + * @name $provide#provider * @description * - * Register a provider for a service. The providers can be retrieved and can have additional configuration methods. + * Register a **provider function** with the {@link auto.$injector $injector}. Provider functions + * are constructor functions, whose instances are responsible for "providing" a factory for a + * service. + * + * Service provider names start with the name of the service they provide followed by `Provider`. + * For example, the {@link ng.$log $log} service has a provider called + * {@link ng.$logProvider $logProvider}. + * + * Service provider objects can have additional methods which allow configuration of the provider + * and its service. Importantly, you can configure what kind of service is created by the `$get` + * method, or how that service will act. For example, the {@link ng.$logProvider $logProvider} has a + * method {@link ng.$logProvider#debugEnabled debugEnabled} + * which lets you specify whether the {@link ng.$log $log} service will log debug messages to the + * console or not. * - * @param {string} name The name of the instance. NOTE: the provider will be available under `name + 'Provider'` key. + * @param {string} name The name of the instance. NOTE: the provider will be available under `name + + 'Provider'` key. * @param {(Object|function())} provider If the provider is: * * - `Object`: then it should have a `$get` method. The `$get` method will be invoked using - * {@link AUTO.$injector#invoke $injector.invoke()} when an instance needs to be created. + * {@link auto.$injector#invoke $injector.invoke()} when an instance needs to be created. * - `Constructor`: a new instance of the provider will be created using - * {@link AUTO.$injector#instantiate $injector.instantiate()}, then treated as `object`. + * {@link auto.$injector#instantiate $injector.instantiate()}, then treated as `object`. * * @returns {Object} registered provider instance + + * @example + * + * The following example shows how to create a simple event tracking service and register it using + * {@link auto.$provide#provider $provide.provider()}. + * + * ```js + * // Define the eventTracker provider + * function EventTrackerProvider() { + * var trackingUrl = '/track'; + * + * // A provider method for configuring where the tracked events should been saved + * this.setTrackingUrl = function(url) { + * trackingUrl = url; + * }; + * + * // The service factory function + * this.$get = ['$http', function($http) { + * var trackedEvents = {}; + * return { + * // Call this to track an event + * event: function(event) { + * var count = trackedEvents[event] || 0; + * count += 1; + * trackedEvents[event] = count; + * return count; + * }, + * // Call this to save the tracked events to the trackingUrl + * save: function() { + * $http.post(trackingUrl, trackedEvents); + * } + * }; + * }]; + * } + * + * describe('eventTracker', function() { + * var postSpy; + * + * beforeEach(module(function($provide) { + * // Register the eventTracker provider + * $provide.provider('eventTracker', EventTrackerProvider); + * })); + * + * beforeEach(module(function(eventTrackerProvider) { + * // Configure eventTracker provider + * eventTrackerProvider.setTrackingUrl('/custom-track'); + * })); + * + * it('tracks events', inject(function(eventTracker) { + * expect(eventTracker.event('login')).toEqual(1); + * expect(eventTracker.event('login')).toEqual(2); + * })); + * + * it('saves to the tracking url', inject(function(eventTracker, $http) { + * postSpy = spyOn($http, 'post'); + * eventTracker.event('login'); + * eventTracker.save(); + * expect(postSpy).toHaveBeenCalled(); + * expect(postSpy.mostRecentCall.args[0]).not.toEqual('/track'); + * expect(postSpy.mostRecentCall.args[0]).toEqual('/custom-track'); + * expect(postSpy.mostRecentCall.args[1]).toEqual({ 'login': 1 }); + * })); + * }); + * ``` */ /** * @ngdoc method - * @name AUTO.$provide#factory - * @methodOf AUTO.$provide + * @name $provide#factory * @description * - * A short hand for configuring services if only `$get` method is required. + * Register a **service factory**, which will be called to return the service instance. + * This is short for registering a service where its provider consists of only a `$get` property, + * which is the given service factory function. + * You should use {@link auto.$provide#factory $provide.factory(getFn)} if you do not need to + * configure your service in a provider. * * @param {string} name The name of the instance. - * @param {function()} $getFn The $getFn for the instance creation. Internally this is a short hand for - * `$provide.provider(name, {$get: $getFn})`. + * @param {function()} $getFn The $getFn for the instance creation. Internally this is a short hand + * for `$provide.provider(name, {$get: $getFn})`. * @returns {Object} registered provider instance + * + * @example + * Here is an example of registering a service + * ```js + * $provide.factory('ping', ['$http', function($http) { + * return function ping() { + * return $http.send('/ping'); + * }; + * }]); + * ``` + * You would then inject and use this service like this: + * ```js + * someModule.controller('Ctrl', ['ping', function(ping) { + * ping(); + * }]); + * ``` */ /** * @ngdoc method - * @name AUTO.$provide#service - * @methodOf AUTO.$provide + * @name $provide#service * @description * - * A short hand for registering service of given class. + * Register a **service constructor**, which will be invoked with `new` to create the service + * instance. + * This is short for registering a service where its provider's `$get` property is the service + * constructor function that will be used to instantiate the service instance. + * + * You should use {@link auto.$provide#service $provide.service(class)} if you define your service + * as a type/class. * * @param {string} name The name of the instance. * @param {Function} constructor A class (constructor function) that will be instantiated. * @returns {Object} registered provider instance + * + * @example + * Here is an example of registering a service using + * {@link auto.$provide#service $provide.service(class)}. + * ```js + * var Ping = function($http) { + * this.$http = $http; + * }; + * + * Ping.$inject = ['$http']; + * + * Ping.prototype.send = function() { + * return this.$http.get('/ping'); + * }; + * $provide.service('ping', Ping); + * ``` + * You would then inject and use this service like this: + * ```js + * someModule.controller('Ctrl', ['ping', function(ping) { + * ping.send(); + * }]); + * ``` */ /** * @ngdoc method - * @name AUTO.$provide#value - * @methodOf AUTO.$provide + * @name $provide#value * @description * - * A short hand for configuring services if the `$get` method is a constant. + * Register a **value service** with the {@link auto.$injector $injector}, such as a string, a + * number, an array, an object or a function. This is short for registering a service where its + * provider's `$get` property is a factory function that takes no arguments and returns the **value + * service**. + * + * Value services are similar to constant services, except that they cannot be injected into a + * module configuration function (see {@link angular.Module#config}) but they can be overridden by + * an Angular + * {@link auto.$provide#decorator decorator}. * * @param {string} name The name of the instance. * @param {*} value The value. * @returns {Object} registered provider instance + * + * @example + * Here are some examples of creating value services. + * ```js + * $provide.value('ADMIN_USER', 'admin'); + * + * $provide.value('RoleLookup', { admin: 0, writer: 1, reader: 2 }); + * + * $provide.value('halfOf', function(value) { + * return value / 2; + * }); + * ``` */ /** * @ngdoc method - * @name AUTO.$provide#constant - * @methodOf AUTO.$provide + * @name $provide#constant * @description * - * A constant value, but unlike {@link AUTO.$provide#value value} it can be injected - * into configuration function (other modules) and it is not interceptable by - * {@link AUTO.$provide#decorator decorator}. + * Register a **constant service**, such as a string, a number, an array, an object or a function, + * with the {@link auto.$injector $injector}. Unlike {@link auto.$provide#value value} it can be + * injected into a module configuration function (see {@link angular.Module#config}) and it cannot + * be overridden by an Angular {@link auto.$provide#decorator decorator}. * * @param {string} name The name of the constant. * @param {*} value The constant value. * @returns {Object} registered instance + * + * @example + * Here a some examples of creating constants: + * ```js + * $provide.constant('SHARD_HEIGHT', 306); + * + * $provide.constant('MY_COLOURS', ['red', 'blue', 'grey']); + * + * $provide.constant('double', function(value) { + * return value * 2; + * }); + * ``` */ /** * @ngdoc method - * @name AUTO.$provide#decorator - * @methodOf AUTO.$provide + * @name $provide#decorator * @description * - * Decoration of service, allows the decorator to intercept the service instance creation. The - * returned instance may be the original instance, or a new instance which delegates to the - * original instance. + * Register a **service decorator** with the {@link auto.$injector $injector}. A service decorator + * intercepts the creation of a service, allowing it to override or modify the behaviour of the + * service. The object returned by the decorator may be the original service, or a new service + * object which replaces or wraps and delegates to the original service. * * @param {string} name The name of the service to decorate. * @param {function()} decorator This function will be invoked when the service needs to be - * instantiated. The function is called using the {@link AUTO.$injector#invoke - * injector.invoke} method and is therefore fully injectable. Local injection arguments: + * instantiated and should return the decorated service instance. The function is called using + * the {@link auto.$injector#invoke injector.invoke} method and is therefore fully injectable. + * Local injection arguments: * * * `$delegate` - The original service instance, which can be monkey patched, configured, * decorated or delegated to. + * + * @example + * Here we decorate the {@link ng.$log $log} service to convert warnings to errors by intercepting + * calls to {@link ng.$log#error $log.warn()}. + * ```js + * $provide.decorator('$log', ['$delegate', function($delegate) { + * $delegate.warn = $delegate.error; + * return $delegate; + * }]); + * ``` */ @@ -2719,7 +3785,7 @@ function createInjector(modulesToLoad) { var INSTANTIATING = {}, providerSuffix = 'Provider', path = [], - loadedModules = new HashMap(), + loadedModules = new HashMap([], true), providerCache = { $provide: { provider: supportObject(provider), @@ -2730,9 +3796,10 @@ function createInjector(modulesToLoad) { decorator: decorator } }, - providerInjector = createInternalInjector(providerCache, function() { - throw Error("Unknown provider: " + path.join(' <- ')); - }), + providerInjector = (providerCache.$injector = + createInternalInjector(providerCache, function() { + throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- ')); + })), instanceCache = {}, instanceInjector = (instanceCache.$injector = createInternalInjector(instanceCache, function(servicename) { @@ -2756,15 +3823,16 @@ function createInjector(modulesToLoad) { } else { return delegate(key, value); } - } + }; } function provider(name, provider_) { + assertNotHasOwnProperty(name, 'service'); if (isFunction(provider_) || isArray(provider_)) { provider_ = providerInjector.instantiate(provider_); } if (!provider_.$get) { - throw Error('Provider ' + name + ' must define $get factory method.'); + throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name); } return providerCache[name + providerSuffix] = provider_; } @@ -2777,9 +3845,10 @@ function createInjector(modulesToLoad) { }]); } - function value(name, value) { return factory(name, valueFn(value)); } + function value(name, val) { return factory(name, valueFn(val)); } function constant(name, value) { + assertNotHasOwnProperty(name, 'constant'); providerCache[name] = value; instanceCache[name] = value; } @@ -2798,43 +3867,43 @@ function createInjector(modulesToLoad) { // Module Loading //////////////////////////////////// function loadModules(modulesToLoad){ - var runBlocks = []; + var runBlocks = [], moduleFn, invokeQueue, i, ii; forEach(modulesToLoad, function(module) { if (loadedModules.get(module)) return; loadedModules.put(module, true); - if (isString(module)) { - var moduleFn = angularModule(module); - runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks); - try { - for(var invokeQueue = moduleFn._invokeQueue, i = 0, ii = invokeQueue.length; i < ii; i++) { + try { + if (isString(module)) { + moduleFn = angularModule(module); + runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks); + + for(invokeQueue = moduleFn._invokeQueue, i = 0, ii = invokeQueue.length; i < ii; i++) { var invokeArgs = invokeQueue[i], - provider = invokeArgs[0] == '$injector' - ? providerInjector - : providerInjector.get(invokeArgs[0]); + provider = providerInjector.get(invokeArgs[0]); provider[invokeArgs[1]].apply(provider, invokeArgs[2]); } - } catch (e) { - if (e.message) e.message += ' from ' + module; - throw e; + } else if (isFunction(module)) { + runBlocks.push(providerInjector.invoke(module)); + } else if (isArray(module)) { + runBlocks.push(providerInjector.invoke(module)); + } else { + assertArgFn(module, 'module'); } - } else if (isFunction(module)) { - try { - runBlocks.push(providerInjector.invoke(module)); - } catch (e) { - if (e.message) e.message += ' from ' + module; - throw e; + } catch (e) { + if (isArray(module)) { + module = module[module.length - 1]; } - } else if (isArray(module)) { - try { - runBlocks.push(providerInjector.invoke(module)); - } catch (e) { - if (e.message) e.message += ' from ' + String(module[module.length - 1]); - throw e; + if (e.message && e.stack && e.stack.indexOf(e.message) == -1) { + // Safari & FF's stack traces don't contain error.message content + // unlike those of Chrome and IE + // So if stack doesn't contain message, we create a new string that contains both. + // Since error.stack is read-only in Safari, I'm overriding e and not e.stack here. + /* jshint -W022 */ + e = e.message + '\n' + e.stack; } - } else { - assertArgFn(module, 'module'); + throw $injectorMinErr('modulerr', "Failed to instantiate module {0} due to:\n{1}", + module, e.stack || e.message || e); } }); return runBlocks; @@ -2847,12 +3916,10 @@ function createInjector(modulesToLoad) { function createInternalInjector(cache, factory) { function getService(serviceName) { - if (typeof serviceName !== 'string') { - throw Error('Service name expected'); - } if (cache.hasOwnProperty(serviceName)) { if (cache[serviceName] === INSTANTIATING) { - throw Error('Circular dependency: ' + path.join(' <- ')); + throw $injectorMinErr('cdep', 'Circular dependency found: {0}', + serviceName + ' <- ' + path.join(' <- ')); } return cache[serviceName]; } else { @@ -2860,6 +3927,11 @@ function createInjector(modulesToLoad) { path.unshift(serviceName); cache[serviceName] = INSTANTIATING; return cache[serviceName] = factory(serviceName); + } catch (err) { + if (cache[serviceName] === INSTANTIATING) { + delete cache[serviceName]; + } + throw err; } finally { path.shift(); } @@ -2874,33 +3946,23 @@ function createInjector(modulesToLoad) { for(i = 0, length = $inject.length; i < length; i++) { key = $inject[i]; + if (typeof key !== 'string') { + throw $injectorMinErr('itkn', + 'Incorrect injection token! Expected service name as string, got {0}', key); + } args.push( locals && locals.hasOwnProperty(key) ? locals[key] : getService(key) ); } - if (!fn.$inject) { - // this means that we must be an array. + if (isArray(fn)) { fn = fn[length]; } - - // Performance optimization: http://jsperf.com/apply-vs-call-vs-invoke - switch (self ? -1 : args.length) { - case 0: return fn(); - case 1: return fn(args[0]); - case 2: return fn(args[0], args[1]); - case 3: return fn(args[0], args[1], args[2]); - case 4: return fn(args[0], args[1], args[2], args[3]); - case 5: return fn(args[0], args[1], args[2], args[3], args[4]); - case 6: return fn(args[0], args[1], args[2], args[3], args[4], args[5]); - case 7: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6]); - case 8: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]); - case 9: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]); - case 10: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]); - default: return fn.apply(self, args); - } + // http://jsperf.com/angularjs-invoke-apply-vs-switch + // #5388 + return fn.apply(self, args); } function instantiate(Type, locals) { @@ -2913,37 +3975,87 @@ function createInjector(modulesToLoad) { instance = new Constructor(); returnedValue = invoke(Type, instance, locals); - return isObject(returnedValue) ? returnedValue : instance; + return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance; } return { invoke: invoke, instantiate: instantiate, get: getService, - annotate: annotate + annotate: annotate, + has: function(name) { + return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name); + } }; } } /** - * @ngdoc function - * @name ng.$anchorScroll + * @ngdoc service + * @name $anchorScroll + * @kind function * @requires $window * @requires $location * @requires $rootScope * * @description - * When called, it checks current value of `$location.hash()` and scroll to related element, + * When called, it checks current value of `$location.hash()` and scrolls to the related element, * according to rules specified in - * {@link http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document Html5 spec}. + * [Html5 spec](http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document). * - * It also watches the `$location.hash()` and scroll whenever it changes to match any anchor. + * It also watches the `$location.hash()` and scrolls whenever it changes to match any anchor. * This can be disabled by calling `$anchorScrollProvider.disableAutoScrolling()`. + * + * @example + + +
+ Go to bottom + You're at the bottom! +
+
+ + function ScrollCtrl($scope, $location, $anchorScroll) { + $scope.gotoBottom = function (){ + // set the location.hash to the id of + // the element you wish to scroll to. + $location.hash('bottom'); + + // call $anchorScroll() + $anchorScroll(); + }; + } + + + #scrollArea { + height: 350px; + overflow: auto; + } + + #bottom { + display: block; + margin-top: 2000px; + } + +
*/ function $AnchorScrollProvider() { var autoScrollingEnabled = true; + /** + * @ngdoc method + * @name $anchorScrollProvider#disableAutoScrolling + * + * @description + * By default, {@link ng.$anchorScroll $anchorScroll()} will automatically detect changes to + * {@link ng.$location#hash $location.hash()} and scroll to the element matching the new hash.
+ * Use this method to disable automatic scrolling. + * + * If automatic scrolling is disabled, one must explicitly call + * {@link ng.$anchorScroll $anchorScroll()} in order to scroll to the element related to the + * current hash. + */ this.disableAutoScrolling = function() { autoScrollingEnabled = false; }; @@ -2992,72 +4104,330 @@ function $AnchorScrollProvider() { }]; } +var $animateMinErr = minErr('$animate'); + /** - * ! This is a private undocumented service ! + * @ngdoc provider + * @name $animateProvider * - * @name ng.$browser - * @requires $log * @description - * This object has two goals: + * Default implementation of $animate that doesn't perform any animations, instead just + * synchronously performs DOM + * updates and calls done() callbacks. * - * - hide all the global state in the browser caused by the window object - * - abstract away all the browser specific features and inconsistencies + * In order to enable animations the ngAnimate module has to be loaded. * - * For tests we provide {@link ngMock.$browser mock implementation} of the `$browser` - * service, which can be used for convenient testing of the application without the interaction with - * the real browser apis. - */ -/** - * @param {object} window The global window object. - * @param {object} document jQuery wrapped document. - * @param {function()} XHR XMLHttpRequest constructor. - * @param {object} $log console.log or an object with the same interface. - * @param {object} $sniffer $sniffer service + * To see the functional implementation check out src/ngAnimate/animate.js */ -function Browser(window, document, $log, $sniffer) { - var self = this, - rawDocument = document[0], - location = window.location, - history = window.history, - setTimeout = window.setTimeout, - clearTimeout = window.clearTimeout, - pendingDeferIds = {}; +var $AnimateProvider = ['$provide', function($provide) { - self.isMock = false; - var outstandingRequestCount = 0; - var outstandingRequestCallbacks = []; + this.$$selectors = {}; - // TODO(vojta): remove this temporary api - self.$$completeOutstandingRequest = completeOutstandingRequest; - self.$$incOutstandingRequestCount = function() { outstandingRequestCount++; }; /** - * Executes the `fn` function(supports currying) and decrements the `outstandingRequestCallbacks` - * counter. If the counter reaches 0, all the `outstandingRequestCallbacks` are executed. + * @ngdoc method + * @name $animateProvider#register + * + * @description + * Registers a new injectable animation factory function. The factory function produces the + * animation object which contains callback functions for each event that is expected to be + * animated. + * + * * `eventFn`: `function(Element, doneFunction)` The element to animate, the `doneFunction` + * must be called once the element animation is complete. If a function is returned then the + * animation service will use this function to cancel the animation whenever a cancel event is + * triggered. + * + * + * ```js + * return { + * eventFn : function(element, done) { + * //code to run the animation + * //once complete, then run done() + * return function cancellationFunction() { + * //code to cancel the animation + * } + * } + * } + * ``` + * + * @param {string} name The name of the animation. + * @param {Function} factory The factory function that will be executed to return the animation + * object. */ - function completeOutstandingRequest(fn) { - try { - fn.apply(null, sliceArgs(arguments, 1)); - } finally { - outstandingRequestCount--; - if (outstandingRequestCount === 0) { - while(outstandingRequestCallbacks.length) { - try { - outstandingRequestCallbacks.pop()(); - } catch (e) { - $log.error(e); - } - } - } - } - } + this.register = function(name, factory) { + var key = name + '-animation'; + if (name && name.charAt(0) != '.') throw $animateMinErr('notcsel', + "Expecting class selector starting with '.' got '{0}'.", name); + this.$$selectors[name.substr(1)] = key; + $provide.factory(key, factory); + }; /** - * @private - * Note: this method is used only by scenario runner - * TODO(vojta): prefix this method with $$ ? - * @param {function()} callback Function that will be called when no outstanding request + * @ngdoc method + * @name $animateProvider#classNameFilter + * + * @description + * Sets and/or returns the CSS class regular expression that is checked when performing + * an animation. Upon bootstrap the classNameFilter value is not set at all and will + * therefore enable $animate to attempt to perform an animation on any element. + * When setting the classNameFilter value, animations will only be performed on elements + * that successfully match the filter expression. This in turn can boost performance + * for low-powered devices as well as applications containing a lot of structural operations. + * @param {RegExp=} expression The className expression which will be checked against all animations + * @return {RegExp} The current CSS className expression value. If null then there is no expression value + */ + this.classNameFilter = function(expression) { + if(arguments.length === 1) { + this.$$classNameFilter = (expression instanceof RegExp) ? expression : null; + } + return this.$$classNameFilter; + }; + + this.$get = ['$timeout', '$$asyncCallback', function($timeout, $$asyncCallback) { + + function async(fn) { + fn && $$asyncCallback(fn); + } + + /** + * + * @ngdoc service + * @name $animate + * @description The $animate service provides rudimentary DOM manipulation functions to + * insert, remove and move elements within the DOM, as well as adding and removing classes. + * This service is the core service used by the ngAnimate $animator service which provides + * high-level animation hooks for CSS and JavaScript. + * + * $animate is available in the AngularJS core, however, the ngAnimate module must be included + * to enable full out animation support. Otherwise, $animate will only perform simple DOM + * manipulation operations. + * + * To learn more about enabling animation support, click here to visit the {@link ngAnimate + * ngAnimate module page} as well as the {@link ngAnimate.$animate ngAnimate $animate service + * page}. + */ + return { + + /** + * + * @ngdoc method + * @name $animate#enter + * @kind function + * @description Inserts the element into the DOM either after the `after` element or within + * the `parent` element. Once complete, the done() callback will be fired (if provided). + * @param {DOMElement} element the element which will be inserted into the DOM + * @param {DOMElement} parent the parent element which will append the element as + * a child (if the after element is not present) + * @param {DOMElement} after the sibling element which will append the element + * after itself + * @param {Function=} done callback function that will be called after the element has been + * inserted into the DOM + */ + enter : function(element, parent, after, done) { + if (after) { + after.after(element); + } else { + if (!parent || !parent[0]) { + parent = after.parent(); + } + parent.append(element); + } + async(done); + }, + + /** + * + * @ngdoc method + * @name $animate#leave + * @kind function + * @description Removes the element from the DOM. Once complete, the done() callback will be + * fired (if provided). + * @param {DOMElement} element the element which will be removed from the DOM + * @param {Function=} done callback function that will be called after the element has been + * removed from the DOM + */ + leave : function(element, done) { + element.remove(); + async(done); + }, + + /** + * + * @ngdoc method + * @name $animate#move + * @kind function + * @description Moves the position of the provided element within the DOM to be placed + * either after the `after` element or inside of the `parent` element. Once complete, the + * done() callback will be fired (if provided). + * + * @param {DOMElement} element the element which will be moved around within the + * DOM + * @param {DOMElement} parent the parent element where the element will be + * inserted into (if the after element is not present) + * @param {DOMElement} after the sibling element where the element will be + * positioned next to + * @param {Function=} done the callback function (if provided) that will be fired after the + * element has been moved to its new position + */ + move : function(element, parent, after, done) { + // Do not remove element before insert. Removing will cause data associated with the + // element to be dropped. Insert will implicitly do the remove. + this.enter(element, parent, after, done); + }, + + /** + * + * @ngdoc method + * @name $animate#addClass + * @kind function + * @description Adds the provided className CSS class value to the provided element. Once + * complete, the done() callback will be fired (if provided). + * @param {DOMElement} element the element which will have the className value + * added to it + * @param {string} className the CSS class which will be added to the element + * @param {Function=} done the callback function (if provided) that will be fired after the + * className value has been added to the element + */ + addClass : function(element, className, done) { + className = isString(className) ? + className : + isArray(className) ? className.join(' ') : ''; + forEach(element, function (element) { + jqLiteAddClass(element, className); + }); + async(done); + }, + + /** + * + * @ngdoc method + * @name $animate#removeClass + * @kind function + * @description Removes the provided className CSS class value from the provided element. + * Once complete, the done() callback will be fired (if provided). + * @param {DOMElement} element the element which will have the className value + * removed from it + * @param {string} className the CSS class which will be removed from the element + * @param {Function=} done the callback function (if provided) that will be fired after the + * className value has been removed from the element + */ + removeClass : function(element, className, done) { + className = isString(className) ? + className : + isArray(className) ? className.join(' ') : ''; + forEach(element, function (element) { + jqLiteRemoveClass(element, className); + }); + async(done); + }, + + /** + * + * @ngdoc method + * @name $animate#setClass + * @kind function + * @description Adds and/or removes the given CSS classes to and from the element. + * Once complete, the done() callback will be fired (if provided). + * @param {DOMElement} element the element which will have its CSS classes changed + * removed from it + * @param {string} add the CSS classes which will be added to the element + * @param {string} remove the CSS class which will be removed from the element + * @param {Function=} done the callback function (if provided) that will be fired after the + * CSS classes have been set on the element + */ + setClass : function(element, add, remove, done) { + forEach(element, function (element) { + jqLiteAddClass(element, add); + jqLiteRemoveClass(element, remove); + }); + async(done); + }, + + enabled : noop + }; + }]; +}]; + +function $$AsyncCallbackProvider(){ + this.$get = ['$$rAF', '$timeout', function($$rAF, $timeout) { + return $$rAF.supported + ? function(fn) { return $$rAF(fn); } + : function(fn) { + return $timeout(fn, 0, false); + }; + }]; +} + +/* global stripHash: true */ + +/** + * ! This is a private undocumented service ! + * + * @name $browser + * @requires $log + * @description + * This object has two goals: + * + * - hide all the global state in the browser caused by the window object + * - abstract away all the browser specific features and inconsistencies + * + * For tests we provide {@link ngMock.$browser mock implementation} of the `$browser` + * service, which can be used for convenient testing of the application without the interaction with + * the real browser apis. + */ +/** + * @param {object} window The global window object. + * @param {object} document jQuery wrapped document. + * @param {function()} XHR XMLHttpRequest constructor. + * @param {object} $log console.log or an object with the same interface. + * @param {object} $sniffer $sniffer service + */ +function Browser(window, document, $log, $sniffer) { + var self = this, + rawDocument = document[0], + location = window.location, + history = window.history, + setTimeout = window.setTimeout, + clearTimeout = window.clearTimeout, + pendingDeferIds = {}; + + self.isMock = false; + + var outstandingRequestCount = 0; + var outstandingRequestCallbacks = []; + + // TODO(vojta): remove this temporary api + self.$$completeOutstandingRequest = completeOutstandingRequest; + self.$$incOutstandingRequestCount = function() { outstandingRequestCount++; }; + + /** + * Executes the `fn` function(supports currying) and decrements the `outstandingRequestCallbacks` + * counter. If the counter reaches 0, all the `outstandingRequestCallbacks` are executed. + */ + function completeOutstandingRequest(fn) { + try { + fn.apply(null, sliceArgs(arguments, 1)); + } finally { + outstandingRequestCount--; + if (outstandingRequestCount === 0) { + while(outstandingRequestCallbacks.length) { + try { + outstandingRequestCallbacks.pop()(); + } catch (e) { + $log.error(e); + } + } + } + } + } + + /** + * @private + * Note: this method is used only by scenario runner + * TODO(vojta): prefix this method with $$ ? + * @param {function()} callback Function that will be called when no outstanding request */ self.notifyWhenNoOutstandingRequests = function(callback) { // force browser to execute all pollFns - this is needed so that cookies and other pollers fire @@ -3079,8 +4449,7 @@ function Browser(window, document, $log, $sniffer) { pollTimeout; /** - * @name ng.$browser#addPollFn - * @methodOf ng.$browser + * @name $browser#addPollFn * * @param {function()} fn Poll function to add * @@ -3116,11 +4485,11 @@ function Browser(window, document, $log, $sniffer) { ////////////////////////////////////////////////////////////// var lastBrowserUrl = location.href, - baseElement = document.find('base'); + baseElement = document.find('base'), + reloadLocation = null; /** - * @name ng.$browser#url - * @methodOf ng.$browser + * @name $browser#url * * @description * GETTER: @@ -3139,11 +4508,20 @@ function Browser(window, document, $log, $sniffer) { * @param {boolean=} replace Should new url replace current history record ? */ self.url = function(url, replace) { + // Android Browser BFCache causes location, history reference to become stale. + if (location !== window.location) location = window.location; + if (history !== window.history) history = window.history; + // setter if (url) { if (lastBrowserUrl == url) return; + var sameBase = lastBrowserUrl && stripHash(lastBrowserUrl) === stripHash(url); lastBrowserUrl = url; - if ($sniffer.history) { + // Don't use history API if only the hash changed + // due to a bug in IE10/IE11 which leads + // to not firing a `hashchange` nor `popstate` event + // in some cases (see #9143). + if (!sameBase && $sniffer.history) { if (replace) history.replaceState(null, '', url); else { history.pushState(null, '', url); @@ -3151,14 +4529,22 @@ function Browser(window, document, $log, $sniffer) { baseElement.attr('href', baseElement.attr('href')); } } else { - if (replace) location.replace(url); - else location.href = url; + if (!sameBase) { + reloadLocation = url; + } + if (replace) { + location.replace(url); + } else { + location.href = url; + } } return self; // getter } else { - // the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172 - return location.href.replace(/%27/g,"'"); + // - reloadLocation is needed as browsers don't allow to read out + // the new location.href if a reload happened. + // - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172 + return reloadLocation || location.href.replace(/%27/g,"'"); } }; @@ -3175,14 +4561,12 @@ function Browser(window, document, $log, $sniffer) { } /** - * @name ng.$browser#onUrlChange - * @methodOf ng.$browser - * @TODO(vojta): refactor to use node's syntax for events + * @name $browser#onUrlChange * * @description * Register callback function that will be called, when url changes. * - * It's only called when the url is changed by outside of angular: + * It's only called when the url is changed from outside of angular: * - user types different url into address bar * - user clicks on history (forward/back) button * - user clicks on a link @@ -3198,15 +4582,16 @@ function Browser(window, document, $log, $sniffer) { * @return {function(string)} Returns the registered listener fn - handy if the fn is anonymous. */ self.onUrlChange = function(callback) { + // TODO(vojta): refactor to use node's syntax for events if (!urlChangeInit) { // We listen on both (hashchange/popstate) when available, as some browsers (e.g. Opera) // don't fire popstate when user change the address bar and don't fire hashchange when url // changed by push/replaceState // html5 history api - popstate event - if ($sniffer.history) jqLite(window).bind('popstate', fireUrlChange); + if ($sniffer.history) jqLite(window).on('popstate', fireUrlChange); // hashchange event - if ($sniffer.hashchange) jqLite(window).bind('hashchange', fireUrlChange); + if ($sniffer.hashchange) jqLite(window).on('hashchange', fireUrlChange); // polling else self.addPollFn(fireUrlChange); @@ -3217,19 +4602,29 @@ function Browser(window, document, $log, $sniffer) { return callback; }; + /** + * Checks whether the url has changed outside of Angular. + * Needs to be exported to be able to check for changes that have been done in sync, + * as hashchange/popstate events fire in async. + */ + self.$$checkUrlChange = fireUrlChange; + ////////////////////////////////////////////////////////////// // Misc API ////////////////////////////////////////////////////////////// /** + * @name $browser#baseHref + * + * @description * Returns current * (always relative - without domain) * - * @returns {string=} + * @returns {string} The current base href */ self.baseHref = function() { var href = baseElement.attr('href'); - return href ? href.replace(/^https?\:\/\/[^\/]*/, '') : ''; + return href ? href.replace(/^(https?\:)?\/\/[^\/]*/, '') : ''; }; ////////////////////////////////////////////////////////////// @@ -3240,41 +4635,45 @@ function Browser(window, document, $log, $sniffer) { var cookiePath = self.baseHref(); /** - * @name ng.$browser#cookies - * @methodOf ng.$browser + * @name $browser#cookies * * @param {string=} name Cookie name - * @param {string=} value Cokkie value + * @param {string=} value Cookie value * * @description * The cookies method provides a 'private' low level access to browser cookies. * It is not meant to be used directly, use the $cookie service instead. * * The return values vary depending on the arguments that the method was called with as follows: - *
    - *
  • cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify it
  • - *
  • cookies(name, value) -> set name to value, if value is undefined delete the cookie
  • - *
  • cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that way)
  • - *
+ * + * - cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify + * it + * - cookies(name, value) -> set name to value, if value is undefined delete the cookie + * - cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that + * way) * * @returns {Object} Hash of all cookies (if called without any parameter) */ self.cookies = function(name, value) { + /* global escape: false, unescape: false */ var cookieLength, cookieArray, cookie, i, index; if (name) { if (value === undefined) { - rawDocument.cookie = escape(name) + "=;path=" + cookiePath + ";expires=Thu, 01 Jan 1970 00:00:00 GMT"; + rawDocument.cookie = escape(name) + "=;path=" + cookiePath + + ";expires=Thu, 01 Jan 1970 00:00:00 GMT"; } else { if (isString(value)) { - cookieLength = (rawDocument.cookie = escape(name) + '=' + escape(value) + ';path=' + cookiePath).length + 1; + cookieLength = (rawDocument.cookie = escape(name) + '=' + escape(value) + + ';path=' + cookiePath).length + 1; // per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum: // - 300 cookies // - 20 cookies per unique domain // - 4096 bytes per cookie if (cookieLength > 4096) { - $log.warn("Cookie '"+ name +"' possibly not set or overflowed because it was too large ("+ + $log.warn("Cookie '"+ name + + "' possibly not set or overflowed because it was too large ("+ cookieLength + " > 4096 bytes)!"); } } @@ -3289,7 +4688,7 @@ function Browser(window, document, $log, $sniffer) { cookie = cookieArray[i]; index = cookie.indexOf('='); if (index > 0) { //ignore nameless cookies - var name = unescape(cookie.substring(0, index)); + name = unescape(cookie.substring(0, index)); // the first value that is seen for a cookie is the most // specific one. values for the same cookie name that // follow are for less specific paths. @@ -3305,14 +4704,13 @@ function Browser(window, document, $log, $sniffer) { /** - * @name ng.$browser#defer - * @methodOf ng.$browser - * @param {function()} fn A function, who's execution should be defered. + * @name $browser#defer + * @param {function()} fn A function, who's execution should be deferred. * @param {number=} [delay=0] of milliseconds to defer the function execution. * @returns {*} DeferId that can be used to cancel the task via `$browser.defer.cancel()`. * * @description - * Executes a fn asynchroniously via `setTimeout(fn, delay)`. + * Executes a fn asynchronously via `setTimeout(fn, delay)`. * * Unlike when calling `setTimeout` directly, in test this function is mocked and instead of using * `setTimeout` in tests, the fns are queued in an array, which can be programmatically flushed @@ -3332,14 +4730,14 @@ function Browser(window, document, $log, $sniffer) { /** - * @name ng.$browser#defer.cancel - * @methodOf ng.$browser.defer + * @name $browser#defer.cancel * * @description - * Cancels a defered task identified with `deferId`. + * Cancels a deferred task identified with `deferId`. * * @param {*} deferId Token returned by the `$browser.defer` function. - * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfuly canceled. + * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully + * canceled. */ self.defer.cancel = function(deferId) { if (pendingDeferIds[deferId]) { @@ -3361,11 +4759,26 @@ function $BrowserProvider(){ } /** - * @ngdoc object - * @name ng.$cacheFactory + * @ngdoc service + * @name $cacheFactory * * @description - * Factory that constructs cache objects. + * Factory that constructs {@link $cacheFactory.Cache Cache} objects and gives access to + * them. + * + * ```js + * + * var cache = $cacheFactory('cacheId'); + * expect($cacheFactory.get('cacheId')).toBe(cache); + * expect($cacheFactory.get('noSuchCacheId')).not.toBeDefined(); + * + * cache.put("key", "value"); + * cache.put("another key", "another value"); + * + * // We've specified no options on creation + * expect(cache.info()).toEqual({id: 'cacheId', size: 2}); + * + * ``` * * * @param {string} cacheId Name or id of the newly created cache. @@ -3376,12 +4789,55 @@ function $BrowserProvider(){ * @returns {object} Newly created cache object with the following set of methods: * * - `{object}` `info()` — Returns id, size, and options of cache. - * - `{void}` `put({string} key, {*} value)` — Puts a new key-value pair into the cache. + * - `{{*}}` `put({string} key, {*} value)` — Puts a new key-value pair into the cache and returns + * it. * - `{{*}}` `get({string} key)` — Returns cached value for `key` or undefined for cache miss. * - `{void}` `remove({string} key)` — Removes a key-value pair from the cache. * - `{void}` `removeAll()` — Removes all cached values. * - `{void}` `destroy()` — Removes references to this cache from $cacheFactory. * + * @example + + +
+ + + + +

Cached Values

+
+ + : + +
+ +

Cache Info

+
+ + : + +
+
+
+ + angular.module('cacheExampleApp', []). + controller('CacheController', ['$scope', '$cacheFactory', function($scope, $cacheFactory) { + $scope.keys = []; + $scope.cache = $cacheFactory('cacheId'); + $scope.put = function(key, value) { + if ($scope.cache.get(key) === undefined) { + $scope.keys.push(key); + } + $scope.cache.put(key, value === undefined ? null : value); + }; + }]); + + + p { + margin: 10px 0 3px; + } + +
*/ function $CacheFactoryProvider() { @@ -3390,7 +4846,7 @@ function $CacheFactoryProvider() { function cacheFactory(cacheId, options) { if (cacheId in caches) { - throw Error('cacheId ' + cacheId + ' taken'); + throw minErr('$cacheFactory')('iid', "CacheId '{0}' is already taken!", cacheId); } var size = 0, @@ -3401,12 +4857,71 @@ function $CacheFactoryProvider() { freshEnd = null, staleEnd = null; + /** + * @ngdoc type + * @name $cacheFactory.Cache + * + * @description + * A cache object used to store and retrieve data, primarily used by + * {@link $http $http} and the {@link ng.directive:script script} directive to cache + * templates and other data. + * + * ```js + * angular.module('superCache') + * .factory('superCache', ['$cacheFactory', function($cacheFactory) { + * return $cacheFactory('super-cache'); + * }]); + * ``` + * + * Example test: + * + * ```js + * it('should behave like a cache', inject(function(superCache) { + * superCache.put('key', 'value'); + * superCache.put('another key', 'another value'); + * + * expect(superCache.info()).toEqual({ + * id: 'super-cache', + * size: 2 + * }); + * + * superCache.remove('another key'); + * expect(superCache.get('another key')).toBeUndefined(); + * + * superCache.removeAll(); + * expect(superCache.info()).toEqual({ + * id: 'super-cache', + * size: 0 + * }); + * })); + * ``` + */ return caches[cacheId] = { + /** + * @ngdoc method + * @name $cacheFactory.Cache#put + * @kind function + * + * @description + * Inserts a named entry into the {@link $cacheFactory.Cache Cache} object to be + * retrieved later, and incrementing the size of the cache if the key was not already + * present in the cache. If behaving like an LRU cache, it will also remove stale + * entries from the set. + * + * It will not insert undefined values into the cache. + * + * @param {string} key the key under which the cached data is stored. + * @param {*} value the value to store alongside the key. If it is undefined, the key + * will not be stored. + * @returns {*} the value stored. + */ put: function(key, value) { - var lruEntry = lruHash[key] || (lruHash[key] = {key: key}); + if (capacity < Number.MAX_VALUE) { + var lruEntry = lruHash[key] || (lruHash[key] = {key: key}); - refresh(lruEntry); + refresh(lruEntry); + } if (isUndefined(value)) return; if (!(key in data)) size++; @@ -3415,35 +4930,70 @@ function $CacheFactoryProvider() { if (size > capacity) { this.remove(staleEnd.key); } - }, + return value; + }, + /** + * @ngdoc method + * @name $cacheFactory.Cache#get + * @kind function + * + * @description + * Retrieves named data stored in the {@link $cacheFactory.Cache Cache} object. + * + * @param {string} key the key of the data to be retrieved + * @returns {*} the value stored. + */ get: function(key) { - var lruEntry = lruHash[key]; + if (capacity < Number.MAX_VALUE) { + var lruEntry = lruHash[key]; - if (!lruEntry) return; + if (!lruEntry) return; - refresh(lruEntry); + refresh(lruEntry); + } return data[key]; }, + /** + * @ngdoc method + * @name $cacheFactory.Cache#remove + * @kind function + * + * @description + * Removes an entry from the {@link $cacheFactory.Cache Cache} object. + * + * @param {string} key the key of the entry to be removed + */ remove: function(key) { - var lruEntry = lruHash[key]; + if (capacity < Number.MAX_VALUE) { + var lruEntry = lruHash[key]; + + if (!lruEntry) return; - if (!lruEntry) return; + if (lruEntry == freshEnd) freshEnd = lruEntry.p; + if (lruEntry == staleEnd) staleEnd = lruEntry.n; + link(lruEntry.n,lruEntry.p); - if (lruEntry == freshEnd) freshEnd = lruEntry.p; - if (lruEntry == staleEnd) staleEnd = lruEntry.n; - link(lruEntry.n,lruEntry.p); + delete lruHash[key]; + } - delete lruHash[key]; delete data[key]; size--; }, + /** + * @ngdoc method + * @name $cacheFactory.Cache#removeAll + * @kind function + * + * @description + * Clears the cache object of any entries. + */ removeAll: function() { data = {}; size = 0; @@ -3452,6 +5002,15 @@ function $CacheFactoryProvider() { }, + /** + * @ngdoc method + * @name $cacheFactory.Cache#destroy + * @kind function + * + * @description + * Destroys the {@link $cacheFactory.Cache Cache} object entirely, + * removing it from the {@link $cacheFactory $cacheFactory} set. + */ destroy: function() { data = null; stats = null; @@ -3460,6 +5019,22 @@ function $CacheFactoryProvider() { }, + /** + * @ngdoc method + * @name $cacheFactory.Cache#info + * @kind function + * + * @description + * Retrieve information regarding a particular {@link $cacheFactory.Cache Cache}. + * + * @returns {object} an object with the following properties: + *
    + *
  • **id**: the id of the cache instance
  • + *
  • **size**: the number of entries kept in the cache instance
  • + *
  • **...**: any additional properties from the options object when creating the + * cache.
  • + *
+ */ info: function() { return extend({}, stats, {size: size}); } @@ -3497,6 +5072,15 @@ function $CacheFactoryProvider() { } + /** + * @ngdoc method + * @name $cacheFactory#info + * + * @description + * Get information about all the caches that have been created + * + * @returns {Object} - key-value map of `cacheId` to the result of calling `cache#info` + */ cacheFactory.info = function() { var info = {}; forEach(caches, function(cache, cacheId) { @@ -3506,6 +5090,16 @@ function $CacheFactoryProvider() { }; + /** + * @ngdoc method + * @name $cacheFactory#get + * + * @description + * Get access to a cache object by the `cacheId` used when it was created. + * + * @param {string} cacheId Name or id of a cache to access. + * @returns {object} Cache object identified by the cacheId or undefined if no such cache. + */ cacheFactory.get = function(cacheId) { return caches[cacheId]; }; @@ -3516,11 +5110,44 @@ function $CacheFactoryProvider() { } /** - * @ngdoc object - * @name ng.$templateCache + * @ngdoc service + * @name $templateCache * * @description - * Cache used for storing html templates. + * The first time a template is used, it is loaded in the template cache for quick retrieval. You + * can load templates directly into the cache in a `script` tag, or by consuming the + * `$templateCache` service directly. + * + * Adding via the `script` tag: + * + * ```html + * + * ``` + * + * **Note:** the `script` tag containing the template does not need to be included in the `head` of + * the document, but it must be a descendent of the {@link ng.$rootElement $rootElement} (IE, + * element with ng-app attribute), otherwise the template will be ignored. + * + * Adding via the $templateCache service: + * + * ```js + * var myApp = angular.module('myApp', []); + * myApp.run(function($templateCache) { + * $templateCache.put('templateId.html', 'This is the content of the template'); + * }); + * ``` + * + * To retrieve the template later, simply use it in your HTML: + * ```html + *
+ * ``` + * + * or get it via Javascript: + * ```js + * $templateCache.get('templateId.html') + * ``` * * See {@link ng.$cacheFactory $cacheFactory}. * @@ -3549,98 +5176,465 @@ function $TemplateCacheProvider() { */ -var NON_ASSIGNABLE_MODEL_EXPRESSION = 'Non-assignable model expression: '; - - /** - * @ngdoc function - * @name ng.$compile - * @function + * @ngdoc service + * @name $compile + * @kind function * * @description - * Compiles a piece of HTML string or DOM into a template and produces a template function, which - * can then be used to link {@link ng.$rootScope.Scope scope} and the template together. + * Compiles an HTML string or DOM into a template and produces a template function, which + * can then be used to link {@link ng.$rootScope.Scope `scope`} and the template together. * - * The compilation is a process of walking the DOM tree and trying to match DOM elements to - * {@link ng.$compileProvider#directive directives}. For each match it - * executes corresponding template function and collects the - * instance functions into a single template function which is then returned. + * The compilation is a process of walking the DOM tree and matching DOM elements to + * {@link ng.$compileProvider#directive directives}. * - * The template function can then be used once to produce the view or as it is the case with - * {@link ng.directive:ngRepeat repeater} many-times, in which - * case each call results in a view that is a DOM clone of the original template. + *
+ * **Note:** This document is an in-depth reference of all directive options. + * For a gentle introduction to directives with examples of common use cases, + * see the {@link guide/directive directive guide}. + *
* - - - -
+


- - + + it('should auto compile', function() { - expect(element('div[compile]').text()).toBe('Hello Angular'); - input('html').enter('{{name}}!'); - expect(element('div[compile]').text()).toBe('Angular!'); + var textarea = $('textarea'); + var output = $('div[compile]'); + // The initial state reads 'Hello Angular'. + expect(output.getText()).toBe('Hello Angular'); + textarea.clear(); + textarea.sendKeys('{{name}}!'); + expect(output.getText()).toBe('Angular!'); }); - - + + * * * @param {string|DOMElement} element Element or HTML string to compile into a template function. - * @param {function(angular.Scope[, cloneAttachFn]} transclude function available to directives. - * @param {number} maxPriority only apply directives lower then given priority (Only effects the + * @param {function(angular.Scope, cloneAttachFn=)} transclude function available to directives. + * @param {number} maxPriority only apply directives lower than given priority (Only effects the * root element(s), not their children) - * @returns {function(scope[, cloneAttachFn])} a link function which is used to bind template + * @returns {function(scope, cloneAttachFn=)} a link function which is used to bind template * (a DOM element/tree) to a scope. Where: * * * `scope` - A {@link ng.$rootScope.Scope Scope} to bind to. * * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the - * `template` and call the `cloneAttachFn` function allowing the caller to attach the - * cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is - * called as:
`cloneAttachFn(clonedElement, scope)` where: + * `template` and call the `cloneAttachFn` function allowing the caller to attach the + * cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is + * called as:
`cloneAttachFn(clonedElement, scope)` where: * * * `clonedElement` - is a clone of the original `element` passed into the compiler. * * `scope` - is the current scope with which the linking function is working with. * - * Calling the linking function returns the element of the template. It is either the original element - * passed in, or the clone of the element if the `cloneAttachFn` is provided. + * Calling the linking function returns the element of the template. It is either the original + * element passed in, or the clone of the element if the `cloneAttachFn` is provided. * * After linking the view is not updated until after a call to $digest which typically is done by * Angular automatically. @@ -3649,71 +5643,75 @@ var NON_ASSIGNABLE_MODEL_EXPRESSION = 'Non-assignable model expression: '; * * - If you are not asking the linking function to clone the template, create the DOM element(s) * before you send them to the compiler and keep this reference around. - *
+ *   ```js
  *     var element = $compile('

{{total}}

')(scope); - *
+ * ``` * * - if on the other hand, you need the element to be cloned, the view reference from the original * example would not point to the clone, but rather to the original template that was cloned. In * this case, you can access the clone via the cloneAttachFn: - *
- *     var templateHTML = angular.element('

{{total}}

'), + * ```js + * var templateElement = angular.element('

{{total}}

'), * scope = ....; * - * var clonedElement = $compile(templateHTML)(scope, function(clonedElement, scope) { + * var clonedElement = $compile(templateElement)(scope, function(clonedElement, scope) { * //attach the clone to DOM document at the right place * }); * - * //now we have reference to the cloned DOM via `clone` - *
+ * //now we have reference to the cloned DOM via `clonedElement` + * ``` * * * For information on how the compiler works, see the * {@link guide/compiler Angular HTML Compiler} section of the Developer Guide. */ +var $compileMinErr = minErr('$compile'); /** - * @ngdoc service - * @name ng.$compileProvider - * @function + * @ngdoc provider + * @name $compileProvider + * @kind function * * @description */ -$CompileProvider.$inject = ['$provide']; -function $CompileProvider($provide) { +$CompileProvider.$inject = ['$provide', '$$sanitizeUriProvider']; +function $CompileProvider($provide, $$sanitizeUriProvider) { var hasDirectives = {}, Suffix = 'Directive', - COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/, - CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/, - MULTI_ROOT_TEMPLATE_ERROR = 'Template must have exactly one root element. was: ', - urlSanitizationWhitelist = /^\s*(https?|ftp|mailto|file):/; + COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w_\-]+)\s+(.*)$/, + CLASS_DIRECTIVE_REGEXP = /(([\d\w_\-]+)(?:\:([^;]+))?;?)/; + // Ref: http://developers.whatwg.org/webappapis.html#event-handler-idl-attributes + // The assumption is that future DOM event attribute names will begin with + // 'on' and be composed of only English letters. + var EVENT_HANDLER_ATTR_REGEXP = /^(on[a-z]+|formaction)$/; /** - * @ngdoc function - * @name ng.$compileProvider#directive - * @methodOf ng.$compileProvider - * @function + * @ngdoc method + * @name $compileProvider#directive + * @kind function * * @description - * Register a new directives with the compiler. + * Register a new directive with the compiler. * - * @param {string} name Name of the directive in camel-case. (ie ngBind which will match as - * ng-bind). - * @param {function} directiveFactory An injectable directive factroy function. See {@link guide/directive} for more - * info. + * @param {string|Object} name Name of the directive in camel-case (i.e. ngBind which + * will match as ng-bind), or an object map of directives where the keys are the + * names and the values are the factories. + * @param {Function|Array} directiveFactory An injectable directive factory function. See + * {@link guide/directive} for more info. * @returns {ng.$compileProvider} Self for chaining. */ this.directive = function registerDirective(name, directiveFactory) { + assertNotHasOwnProperty(name, 'directive'); if (isString(name)) { - assertArg(directiveFactory, 'directive'); + assertArg(directiveFactory, 'directiveFactory'); if (!hasDirectives.hasOwnProperty(name)) { hasDirectives[name] = []; $provide.factory(name + Suffix, ['$injector', '$exceptionHandler', function($injector, $exceptionHandler) { var directives = []; - forEach(hasDirectives[name], function(directiveFactory) { + forEach(hasDirectives[name], function(directiveFactory, index) { try { var directive = $injector.invoke(directiveFactory); if (isFunction(directive)) { @@ -3722,6 +5720,7 @@ function $CompileProvider($provide) { directive.compile = valueFn(directive.link); } directive.priority = directive.priority || 0; + directive.index = index; directive.name = directive.name || name; directive.require = directive.require || (directive.controller && directive.name); directive.restrict = directive.restrict || 'A'; @@ -3742,10 +5741,9 @@ function $CompileProvider($provide) { /** - * @ngdoc function - * @name ng.$compileProvider#urlSanitizationWhitelist - * @methodOf ng.$compileProvider - * @function + * @ngdoc method + * @name $compileProvider#aHrefSanitizationWhitelist + * @kind function * * @description * Retrieves or overrides the default regular expression that is used for whitelisting of safe @@ -3753,29 +5751,59 @@ function $CompileProvider($provide) { * * The sanitization is a security measure aimed at prevent XSS attacks via html links. * - * Any url about to be assigned to a[href] via data-binding is first normalized and turned into an - * absolute url. Afterwards the url is matched against the `urlSanitizationWhitelist` regular - * expression. If a match is found the original url is written into the dom. Otherwise the - * absolute url is prefixed with `'unsafe:'` string and only then it is written into the DOM. + * Any url about to be assigned to a[href] via data-binding is first normalized and turned into + * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist` + * regular expression. If a match is found, the original url is written into the dom. Otherwise, + * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. * * @param {RegExp=} regexp New regexp to whitelist urls with. * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for * chaining otherwise. */ - this.urlSanitizationWhitelist = function(regexp) { + this.aHrefSanitizationWhitelist = function(regexp) { if (isDefined(regexp)) { - urlSanitizationWhitelist = regexp; + $$sanitizeUriProvider.aHrefSanitizationWhitelist(regexp); return this; + } else { + return $$sanitizeUriProvider.aHrefSanitizationWhitelist(); } - return urlSanitizationWhitelist; }; + /** + * @ngdoc method + * @name $compileProvider#imgSrcSanitizationWhitelist + * @kind function + * + * @description + * Retrieves or overrides the default regular expression that is used for whitelisting of safe + * urls during img[src] sanitization. + * + * The sanitization is a security measure aimed at prevent XSS attacks via html links. + * + * Any url about to be assigned to img[src] via data-binding is first normalized and turned into + * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist` + * regular expression. If a match is found, the original url is written into the dom. Otherwise, + * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. + * + * @param {RegExp=} regexp New regexp to whitelist urls with. + * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for + * chaining otherwise. + */ + this.imgSrcSanitizationWhitelist = function(regexp) { + if (isDefined(regexp)) { + $$sanitizeUriProvider.imgSrcSanitizationWhitelist(regexp); + return this; + } else { + return $$sanitizeUriProvider.imgSrcSanitizationWhitelist(); + } + }; + this.$get = [ '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse', - '$controller', '$rootScope', '$document', + '$controller', '$rootScope', '$document', '$sce', '$animate', '$$sanitizeUri', function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse, - $controller, $rootScope, $document) { + $controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) { var Attributes = function(element, attr) { this.$$element = element; @@ -3786,6 +5814,65 @@ function $CompileProvider($provide) { $normalize: directiveNormalize, + /** + * @ngdoc method + * @name $compile.directive.Attributes#$addClass + * @kind function + * + * @description + * Adds the CSS class value specified by the classVal parameter to the element. If animations + * are enabled then an animation will be triggered for the class addition. + * + * @param {string} classVal The className value that will be added to the element + */ + $addClass : function(classVal) { + if(classVal && classVal.length > 0) { + $animate.addClass(this.$$element, classVal); + } + }, + + /** + * @ngdoc method + * @name $compile.directive.Attributes#$removeClass + * @kind function + * + * @description + * Removes the CSS class value specified by the classVal parameter from the element. If + * animations are enabled then an animation will be triggered for the class removal. + * + * @param {string} classVal The className value that will be removed from the element + */ + $removeClass : function(classVal) { + if(classVal && classVal.length > 0) { + $animate.removeClass(this.$$element, classVal); + } + }, + + /** + * @ngdoc method + * @name $compile.directive.Attributes#$updateClass + * @kind function + * + * @description + * Adds and removes the appropriate CSS class values to the element based on the difference + * between the new and old CSS class values (specified as newClasses and oldClasses). + * + * @param {string} newClasses The current CSS className value + * @param {string} oldClasses The former CSS className value + */ + $updateClass : function(newClasses, oldClasses) { + var toAdd = tokenDifference(newClasses, oldClasses); + var toRemove = tokenDifference(oldClasses, newClasses); + + if(toAdd.length === 0) { + $animate.removeClass(this.$$element, toRemove); + } else if(toRemove.length === 0) { + $animate.addClass(this.$$element, toAdd); + } else { + $animate.setClass(this.$$element, toAdd, toRemove); + } + }, + /** * Set a normalized attribute on the element in a way such that all directives * can share the attribute. This function properly handles boolean attributes. @@ -3796,9 +5883,13 @@ function $CompileProvider($provide) { * @param {string=} attrName Optional none normalized name. Defaults to key. */ $set: function(key, value, writeAttr, attrName) { + // TODO: decide whether or not to throw an error if "class" + //is set through this function since it may cause $updateClass to + //become unstable. + var booleanKey = getBooleanAttrName(this.$$element[0], key), - $$observers = this.$$observers, - normalizedVal; + normalizedVal, + nodeName; if (booleanKey) { this.$$element.prop(key, value); @@ -3817,19 +5908,14 @@ function $CompileProvider($provide) { } } + nodeName = nodeName_(this.$$element); - // sanitize a[href] values - if (nodeName_(this.$$element[0]) === 'A' && key === 'href') { - urlSanitizationNode.setAttribute('href', value); - - // href property always returns normalized absolute url, so we can match against that - normalizedVal = urlSanitizationNode.href; - if (!normalizedVal.match(urlSanitizationWhitelist)) { - this[key] = value = 'unsafe:' + normalizedVal; - } + // sanitize a[href] and img[src] values + if ((nodeName === 'A' && key === 'href') || + (nodeName === 'IMG' && key === 'src')) { + this[key] = value = $$sanitizeUri(value, key === 'src'); } - if (writeAttr !== false) { if (value === null || value === undefined) { this.$$element.removeAttr(attrName); @@ -3839,6 +5925,7 @@ function $CompileProvider($provide) { } // fire observers + var $$observers = this.$$observers; $$observers && forEach($$observers[key], function(fn) { try { fn(value); @@ -3850,12 +5937,22 @@ function $CompileProvider($provide) { /** - * Observe an interpolated attribute. - * The observer will never be called, if given attribute is not interpolated. + * @ngdoc method + * @name $compile.directive.Attributes#$observe + * @kind function + * + * @description + * Observes an interpolated attribute. + * + * The observer function will be invoked once during the next `$digest` following + * compilation. The observer is then invoked whenever the interpolated value + * changes. * * @param {string} key Normalized key. (ie ngAttribute) . - * @param {function(*)} fn Function that will be called whenever the attribute value changes. - * @returns {function(*)} the `fn` Function passed in. + * @param {function(interpolatedValue)} fn Function that will be called whenever + the interpolated value of the attribute changes. + * See the {@link guide/directive#Attributes Directives} guide for more info. + * @returns {function()} the `fn` parameter. */ $observe: function(key, fn) { var attrs = this, @@ -3873,34 +5970,39 @@ function $CompileProvider($provide) { } }; - var urlSanitizationNode = $document[0].createElement('a'), - startSymbol = $interpolate.startSymbol(), + var startSymbol = $interpolate.startSymbol(), endSymbol = $interpolate.endSymbol(), denormalizeTemplate = (startSymbol == '{{' || endSymbol == '}}') ? identity : function denormalizeTemplate(template) { return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol); - }; + }, + NG_ATTR_BINDING = /^ngAttr[A-Z]/; return compile; //================================ - function compile($compileNodes, transcludeFn, maxPriority) { + function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, + previousCompileContext) { if (!($compileNodes instanceof jqLite)) { - // jquery always rewraps, whereas we need to preserve the original selector so that we can modify it. + // jquery always rewraps, whereas we need to preserve the original selector so that we can + // modify it. $compileNodes = jqLite($compileNodes); } // We can not compile top level text elements since text nodes can be merged and we will // not be able to attach scope data to them, so we will wrap them in forEach($compileNodes, function(node, index){ if (node.nodeType == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) { - $compileNodes[index] = jqLite(node).wrap('').parent()[0]; + $compileNodes[index] = node = jqLite(node).wrap('').parent()[0]; } }); - var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority); - return function publicLinkFn(scope, cloneConnectFn){ + var compositeLinkFn = + compileNodes($compileNodes, transcludeFn, $compileNodes, + maxPriority, ignoreDirective, previousCompileContext); + safeAddClass($compileNodes, 'ng-scope'); + return function publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn){ assertArg(scope, 'scope'); // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart // and sometimes changes the structure of the DOM. @@ -3908,24 +6010,25 @@ function $CompileProvider($provide) { ? JQLitePrototype.clone.call($compileNodes) // IMPORTANT!!! : $compileNodes; + forEach(transcludeControllers, function(instance, name) { + $linkNode.data('$' + name + 'Controller', instance); + }); + // Attach scope only to non-text nodes. for(var i = 0, ii = $linkNode.length; i addDirective(directives, - directiveNormalize(nodeName_(node).toLowerCase()), 'E', maxPriority); + directiveNormalize(nodeName_(node).toLowerCase()), 'E', maxPriority, ignoreDirective); // iterate over the attributes - for (var attr, name, nName, value, nAttrs = node.attributes, + for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes, j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { + var attrStartName = false; + var attrEndName = false; + attr = nAttrs[j]; - if (attr.specified) { + if (!msie || msie >= 8 || attr.specified) { name = attr.name; + value = trim(attr.value); + + // support ngAttr attribute binding + ngAttrName = directiveNormalize(name); + if (isNgAttr = NG_ATTR_BINDING.test(ngAttrName)) { + name = snake_case(ngAttrName.substr(6), '-'); + } + + var directiveNName = ngAttrName.replace(/(Start|End)$/, ''); + if (ngAttrName === directiveNName + 'Start') { + attrStartName = name; + attrEndName = name.substr(0, name.length - 5) + 'end'; + name = name.substr(0, name.length - 6); + } + nName = directiveNormalize(name.toLowerCase()); attrsMap[nName] = name; - attrs[nName] = value = trim((msie && name == 'href') - ? decodeURIComponent(node.getAttribute(name, 2)) - : attr.value); - if (getBooleanAttrName(node, nName)) { - attrs[nName] = true; // presence means true + if (isNgAttr || !attrs.hasOwnProperty(nName)) { + attrs[nName] = value; + if (getBooleanAttrName(node, nName)) { + attrs[nName] = true; // presence means true + } } addAttrInterpolateDirective(node, directives, value, nName); - addDirective(directives, nName, 'A', maxPriority); + addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, + attrEndName); } } @@ -4068,7 +6223,7 @@ function $CompileProvider($provide) { if (isString(className) && className !== '') { while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) { nName = directiveNormalize(match[2]); - if (addDirective(directives, nName, 'C', maxPriority)) { + if (addDirective(directives, nName, 'C', maxPriority, ignoreDirective)) { attrs[nName] = trim(match[3]); } className = className.substr(match.index + match[0].length); @@ -4083,12 +6238,13 @@ function $CompileProvider($provide) { match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue); if (match) { nName = directiveNormalize(match[1]); - if (addDirective(directives, nName, 'M', maxPriority)) { + if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) { attrs[nName] = trim(match[2]); } } } catch (e) { - // turns out that under some circumstances IE9 throws errors when one attempts to read comment's node value. + // turns out that under some circumstances IE9 throws errors when one attempts to read + // comment's node value. // Just ignore it and continue. (Can't seem to reproduce in test case.) } break; @@ -4098,6 +6254,53 @@ function $CompileProvider($provide) { return directives; } + /** + * Given a node with an directive-start it collects all of the siblings until it finds + * directive-end. + * @param node + * @param attrStart + * @param attrEnd + * @returns {*} + */ + function groupScan(node, attrStart, attrEnd) { + var nodes = []; + var depth = 0; + if (attrStart && node.hasAttribute && node.hasAttribute(attrStart)) { + var startNode = node; + do { + if (!node) { + throw $compileMinErr('uterdir', + "Unterminated attribute, found '{0}' but no matching '{1}' found.", + attrStart, attrEnd); + } + if (node.nodeType == 1 /** Element **/) { + if (node.hasAttribute(attrStart)) depth++; + if (node.hasAttribute(attrEnd)) depth--; + } + nodes.push(node); + node = node.nextSibling; + } while (depth > 0); + } else { + nodes.push(node); + } + + return jqLite(nodes); + } + + /** + * Wrapper for linking function which converts normal linking function into a grouped + * linking function. + * @param linkFn + * @param attrStart + * @param attrEnd + * @returns {Function} + */ + function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) { + return function(scope, element, attrs, controllers, transcludeFn) { + element = groupScan(element[0], attrStart, attrEnd); + return linkFn(scope, element, attrs, controllers, transcludeFn); + }; + } /** * Once the directives have been collected, their compile functions are executed. This method @@ -4108,32 +6311,53 @@ function $CompileProvider($provide) { * this needs to be pre-sorted by priority order. * @param {Node} compileNode The raw DOM node to apply the compile functions to * @param {Object} templateAttrs The shared attribute function - * @param {function(angular.Scope[, cloneAttachFn]} transcludeFn A linking function, where the - * scope argument is auto-generated to the new child of the transcluded parent scope. + * @param {function(angular.Scope, cloneAttachFn=)} transcludeFn A linking function, where the + * scope argument is auto-generated to the new + * child of the transcluded parent scope. * @param {JQLite} jqCollection If we are working on the root of the compile tree then this - * argument has the root jqLite array so that we can replace nodes on it. - * @returns linkFn + * argument has the root jqLite array so that we can replace nodes + * on it. + * @param {Object=} originalReplaceDirective An optional directive that will be ignored when + * compiling the transclusion. + * @param {Array.} preLinkFns + * @param {Array.} postLinkFns + * @param {Object} previousCompileContext Context used for previous compilation of the current + * node + * @returns {Function} linkFn */ - function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, jqCollection) { + function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, + jqCollection, originalReplaceDirective, preLinkFns, postLinkFns, + previousCompileContext) { + previousCompileContext = previousCompileContext || {}; + var terminalPriority = -Number.MAX_VALUE, - preLinkFns = [], - postLinkFns = [], - newScopeDirective = null, - newIsolateScopeDirective = null, - templateDirective = null, + newScopeDirective, + controllerDirectives = previousCompileContext.controllerDirectives, + newIsolateScopeDirective = previousCompileContext.newIsolateScopeDirective, + templateDirective = previousCompileContext.templateDirective, + nonTlbTranscludeDirective = previousCompileContext.nonTlbTranscludeDirective, + hasTranscludeDirective = false, + hasTemplate = false, + hasElementTranscludeDirective = previousCompileContext.hasElementTranscludeDirective, $compileNode = templateAttrs.$$element = jqLite(compileNode), directive, directiveName, $template, - transcludeDirective, + replaceDirective = originalReplaceDirective, childTranscludeFn = transcludeFn, - controllerDirectives, linkFn, directiveValue; // executes all directives on the current element for(var i = 0, ii = directives.length; i < ii; i++) { directive = directives[i]; + var attrStart = directive.$$start; + var attrEnd = directive.$$end; + + // collect multiblock sections + if (attrStart) { + $compileNode = groupScan(compileNode, attrStart, attrEnd); + } $template = undefined; if (terminalPriority > directive.priority) { @@ -4141,18 +6365,23 @@ function $CompileProvider($provide) { } if (directiveValue = directive.scope) { - assertNoDuplicate('isolated scope', newIsolateScopeDirective, directive, $compileNode); - if (isObject(directiveValue)) { - safeAddClass($compileNode, 'ng-isolate-scope'); - newIsolateScopeDirective = directive; - } - safeAddClass($compileNode, 'ng-scope'); newScopeDirective = newScopeDirective || directive; + + // skip the check for directives with async templates, we'll check the derived sync + // directive when the template arrives + if (!directive.templateUrl) { + assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive, + $compileNode); + if (isObject(directiveValue)) { + newIsolateScopeDirective = directive; + } + } } directiveName = directive.name; - if (directiveValue = directive.controller) { + if (!directive.templateUrl && directive.controller) { + directiveValue = directive.controller; controllerDirectives = controllerDirectives || {}; assertNoDuplicate("'" + directiveName + "' controller", controllerDirectives[directiveName], directive, $compileNode); @@ -4160,36 +6389,68 @@ function $CompileProvider($provide) { } if (directiveValue = directive.transclude) { - assertNoDuplicate('transclusion', transcludeDirective, directive, $compileNode); - transcludeDirective = directive; - terminalPriority = directive.priority; + hasTranscludeDirective = true; + + // Special case ngIf and ngRepeat so that we don't complain about duplicate transclusion. + // This option should only be used by directives that know how to safely handle element transclusion, + // where the transcluded nodes are added or replaced after linking. + if (!directive.$$tlb) { + assertNoDuplicate('transclusion', nonTlbTranscludeDirective, directive, $compileNode); + nonTlbTranscludeDirective = directive; + } + if (directiveValue == 'element') { - $template = jqLite(compileNode); + hasElementTranscludeDirective = true; + terminalPriority = directive.priority; + $template = $compileNode; $compileNode = templateAttrs.$$element = - jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' ')); + jqLite(document.createComment(' ' + directiveName + ': ' + + templateAttrs[directiveName] + ' ')); compileNode = $compileNode[0]; - replaceWith(jqCollection, jqLite($template[0]), compileNode); - childTranscludeFn = compile($template, transcludeFn, terminalPriority); + replaceWith(jqCollection, sliceArgs($template), compileNode); + + childTranscludeFn = compile($template, transcludeFn, terminalPriority, + replaceDirective && replaceDirective.name, { + // Don't pass in: + // - controllerDirectives - otherwise we'll create duplicates controllers + // - newIsolateScopeDirective or templateDirective - combining templates with + // element transclusion doesn't make sense. + // + // We need only nonTlbTranscludeDirective so that we prevent putting transclusion + // on the same element more than once. + nonTlbTranscludeDirective: nonTlbTranscludeDirective + }); } else { - $template = jqLite(JQLiteClone(compileNode)).contents(); - $compileNode.html(''); // clear contents + $template = jqLite(jqLiteClone(compileNode)).contents(); + $compileNode.empty(); // clear contents childTranscludeFn = compile($template, transcludeFn); } } - if ((directiveValue = directive.template)) { + if (directive.template) { + hasTemplate = true; assertNoDuplicate('template', templateDirective, directive, $compileNode); templateDirective = directive; + + directiveValue = (isFunction(directive.template)) + ? directive.template($compileNode, templateAttrs) + : directive.template; + directiveValue = denormalizeTemplate(directiveValue); if (directive.replace) { - $template = jqLite('
' + - trim(directiveValue) + - '
').contents(); + replaceDirective = directive; + if (jqLiteIsTextNode(directiveValue)) { + $template = []; + } else { + $template = jqLite(trim(directiveValue)); + } compileNode = $template[0]; if ($template.length != 1 || compileNode.nodeType !== 1) { - throw new Error(MULTI_ROOT_TEMPLATE_ERROR + directiveValue); + throw $compileMinErr('tplrt', + "Template for directive '{0}' must have exactly one root element. {1}", + directiveName, ''); } replaceWith(jqCollection, $compileNode, compileNode); @@ -4198,16 +6459,16 @@ function $CompileProvider($provide) { // combine directives from the original node and from the template: // - take the array of directives for this element - // - split it into two parts, those that were already applied and those that weren't - // - collect directives from the template, add them to the second group and sort them - // - append the second group with new directives to the first group - directives = directives.concat( - collectDirectives( - compileNode, - directives.splice(i + 1, directives.length - (i + 1)), - newTemplateAttrs - ) - ); + // - split it into two parts, those that already applied (processed) and those that weren't (unprocessed) + // - collect directives from the template and sort them by priority + // - combine directives as: processed + template + unprocessed + var templateDirectives = collectDirectives(compileNode, [], newTemplateAttrs); + var unprocessedDirectives = directives.splice(i + 1, directives.length - (i + 1)); + + if (newIsolateScopeDirective) { + markDirectivesAsIsolate(templateDirectives); + } + directives = directives.concat(templateDirectives).concat(unprocessedDirectives); mergeTemplateAttributes(templateAttrs, newTemplateAttrs); ii = directives.length; @@ -4217,19 +6478,29 @@ function $CompileProvider($provide) { } if (directive.templateUrl) { + hasTemplate = true; assertNoDuplicate('template', templateDirective, directive, $compileNode); templateDirective = directive; - nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), - nodeLinkFn, $compileNode, templateAttrs, jqCollection, directive.replace, - childTranscludeFn); + + if (directive.replace) { + replaceDirective = directive; + } + + nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode, + templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, { + controllerDirectives: controllerDirectives, + newIsolateScopeDirective: newIsolateScopeDirective, + templateDirective: templateDirective, + nonTlbTranscludeDirective: nonTlbTranscludeDirective + }); ii = directives.length; } else if (directive.compile) { try { linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn); if (isFunction(linkFn)) { - addLinkFns(null, linkFn); + addLinkFns(null, linkFn, attrStart, attrEnd); } else if (linkFn) { - addLinkFns(linkFn.pre, linkFn.post); + addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd); } } catch (e) { $exceptionHandler(e, startingTag($compileNode)); @@ -4243,27 +6514,41 @@ function $CompileProvider($provide) { } - nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope; - nodeLinkFn.transclude = transcludeDirective && childTranscludeFn; + nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope === true; + nodeLinkFn.transcludeOnThisElement = hasTranscludeDirective; + nodeLinkFn.templateOnThisElement = hasTemplate; + nodeLinkFn.transclude = childTranscludeFn; + + previousCompileContext.hasElementTranscludeDirective = hasElementTranscludeDirective; // might be normal or delayed nodeLinkFn depending on if templateUrl is present return nodeLinkFn; //////////////////// - function addLinkFns(pre, post) { + function addLinkFns(pre, post, attrStart, attrEnd) { if (pre) { + if (attrStart) pre = groupElementsLinkFnWrapper(pre, attrStart, attrEnd); pre.require = directive.require; + pre.directiveName = directiveName; + if (newIsolateScopeDirective === directive || directive.$$isolateScope) { + pre = cloneAndAnnotateFn(pre, {isolateScope: true}); + } preLinkFns.push(pre); } if (post) { + if (attrStart) post = groupElementsLinkFnWrapper(post, attrStart, attrEnd); post.require = directive.require; + post.directiveName = directiveName; + if (newIsolateScopeDirective === directive || directive.$$isolateScope) { + post = cloneAndAnnotateFn(post, {isolateScope: true}); + } postLinkFns.push(post); } } - function getControllers(require, $element) { + function getControllers(directiveName, require, $element, elementControllers) { var value, retrievalMethod = 'data', optional = false; if (isString(require)) { while((value = require.charAt(0)) == '^' || value == '?') { @@ -4273,15 +6558,23 @@ function $CompileProvider($provide) { } optional = optional || value == '?'; } - value = $element[retrievalMethod]('$' + require + 'Controller'); + value = null; + + if (elementControllers && retrievalMethod === 'data') { + value = elementControllers[require]; + } + value = value || $element[retrievalMethod]('$' + require + 'Controller'); + if (!value && !optional) { - throw Error("No controller: " + require); + throw $compileMinErr('ctreq', + "Controller '{0}', required by directive '{1}', can't be found!", + require, directiveName); } return value; } else if (isArray(require)) { value = []; forEach(require, function(require) { - value.push(getControllers(require, $element)); + value.push(getControllers(directiveName, require, $element, elementControllers)); }); } return value; @@ -4289,99 +6582,131 @@ function $CompileProvider($provide) { function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) { - var attrs, $element, i, ii, linkFn, controller; + var attrs, $element, i, ii, linkFn, controller, isolateScope, elementControllers = {}, transcludeFn; - if (compileNode === linkNode) { - attrs = templateAttrs; - } else { - attrs = shallowCopy(templateAttrs, new Attributes(jqLite(linkNode), templateAttrs.$attr)); - } + attrs = (compileNode === linkNode) + ? templateAttrs + : shallowCopy(templateAttrs, new Attributes(jqLite(linkNode), templateAttrs.$attr)); $element = attrs.$$element; if (newIsolateScopeDirective) { - var LOCAL_REGEXP = /^\s*([@=&])\s*(\w*)\s*$/; + var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/; + + isolateScope = scope.$new(true); + + if (templateDirective && (templateDirective === newIsolateScopeDirective || + templateDirective === newIsolateScopeDirective.$$originalDirective)) { + $element.data('$isolateScope', isolateScope); + } else { + $element.data('$isolateScopeNoTemplate', isolateScope); + } - var parentScope = scope.$parent || scope; - forEach(newIsolateScopeDirective.scope, function(definiton, scopeName) { - var match = definiton.match(LOCAL_REGEXP) || [], - attrName = match[2]|| scopeName, + + safeAddClass($element, 'ng-isolate-scope'); + + forEach(newIsolateScopeDirective.scope, function(definition, scopeName) { + var match = definition.match(LOCAL_REGEXP) || [], + attrName = match[3] || scopeName, + optional = (match[2] == '?'), mode = match[1], // @, =, or & lastValue, - parentGet, parentSet; + parentGet, parentSet, compare; - scope.$$isolateBindings[scopeName] = mode + attrName; + isolateScope.$$isolateBindings[scopeName] = mode + attrName; switch (mode) { - case '@': { + case '@': attrs.$observe(attrName, function(value) { - scope[scopeName] = value; + isolateScope[scopeName] = value; }); - attrs.$$observers[attrName].$$scope = parentScope; + attrs.$$observers[attrName].$$scope = scope; + if( attrs[attrName] ) { + // If the attribute has been provided then we trigger an interpolation to ensure + // the value is there for use in the link fn + isolateScope[scopeName] = $interpolate(attrs[attrName])(scope); + } break; - } - case '=': { + case '=': + if (optional && !attrs[attrName]) { + return; + } parentGet = $parse(attrs[attrName]); + if (parentGet.literal) { + compare = equals; + } else { + compare = function(a,b) { return a === b || (a !== a && b !== b); }; + } parentSet = parentGet.assign || function() { // reset the change, or we will throw this exception on every $digest - lastValue = scope[scopeName] = parentGet(parentScope); - throw Error(NON_ASSIGNABLE_MODEL_EXPRESSION + attrs[attrName] + - ' (directive: ' + newIsolateScopeDirective.name + ')'); + lastValue = isolateScope[scopeName] = parentGet(scope); + throw $compileMinErr('nonassign', + "Expression '{0}' used with directive '{1}' is non-assignable!", + attrs[attrName], newIsolateScopeDirective.name); }; - lastValue = scope[scopeName] = parentGet(parentScope); - scope.$watch(function parentValueWatch() { - var parentValue = parentGet(parentScope); - - if (parentValue !== scope[scopeName]) { + lastValue = isolateScope[scopeName] = parentGet(scope); + isolateScope.$watch(function parentValueWatch() { + var parentValue = parentGet(scope); + if (!compare(parentValue, isolateScope[scopeName])) { // we are out of sync and need to copy - if (parentValue !== lastValue) { + if (!compare(parentValue, lastValue)) { // parent changed and it has precedence - lastValue = scope[scopeName] = parentValue; + isolateScope[scopeName] = parentValue; } else { // if the parent can be assigned then do so - parentSet(parentScope, parentValue = lastValue = scope[scopeName]); + parentSet(scope, parentValue = isolateScope[scopeName]); } } - return parentValue; - }); + return lastValue = parentValue; + }, null, parentGet.literal); break; - } - case '&': { + case '&': parentGet = $parse(attrs[attrName]); - scope[scopeName] = function(locals) { - return parentGet(parentScope, locals); + isolateScope[scopeName] = function(locals) { + return parentGet(scope, locals); }; break; - } - default: { - throw Error('Invalid isolate scope definition for directive ' + - newIsolateScopeDirective.name + ': ' + definiton); - } + default: + throw $compileMinErr('iscp', + "Invalid isolate scope definition for directive '{0}'." + + " Definition: {... {1}: '{2}' ...}", + newIsolateScopeDirective.name, scopeName, definition); } }); } - + transcludeFn = boundTranscludeFn && controllersBoundTransclude; if (controllerDirectives) { forEach(controllerDirectives, function(directive) { var locals = { - $scope: scope, + $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope, $element: $element, $attrs: attrs, - $transclude: boundTranscludeFn - }; + $transclude: transcludeFn + }, controllerInstance; controller = directive.controller; if (controller == '@') { controller = attrs[directive.name]; } - $element.data( - '$' + directive.name + 'Controller', - $controller(controller, locals)); + controllerInstance = $controller(controller, locals); + // For directives with element transclusion the element is a comment, + // but jQuery .data doesn't support attaching data to comment nodes as it's hard to + // clean up (http://bugs.jquery.com/ticket/8335). + // Instead, we save the controllers for the element in a local hash and attach to .data + // later, once we have the actual element. + elementControllers[directive.name] = controllerInstance; + if (!hasElementTranscludeDirective) { + $element.data('$' + directive.name + 'Controller', controllerInstance); + } + + if (directive.controllerAs) { + locals.$scope[directive.controllerAs] = controllerInstance; + } }); } @@ -4389,34 +6714,63 @@ function $CompileProvider($provide) { for(i = 0, ii = preLinkFns.length; i < ii; i++) { try { linkFn = preLinkFns[i]; - linkFn(scope, $element, attrs, - linkFn.require && getControllers(linkFn.require, $element)); + linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs, + linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), transcludeFn); } catch (e) { $exceptionHandler(e, startingTag($element)); } } // RECURSION - childLinkFn && childLinkFn(scope, linkNode.childNodes, undefined, boundTranscludeFn); + // We only pass the isolate scope, if the isolate directive has a template, + // otherwise the child elements do not belong to the isolate directive. + var scopeToChild = scope; + if (newIsolateScopeDirective && (newIsolateScopeDirective.template || newIsolateScopeDirective.templateUrl === null)) { + scopeToChild = isolateScope; + } + childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn); // POSTLINKING - for(i = 0, ii = postLinkFns.length; i < ii; i++) { + for(i = postLinkFns.length - 1; i >= 0; i--) { try { linkFn = postLinkFns[i]; - linkFn(scope, $element, attrs, - linkFn.require && getControllers(linkFn.require, $element)); + linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs, + linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), transcludeFn); } catch (e) { $exceptionHandler(e, startingTag($element)); } } - } - } + // This is the function that is injected as `$transclude`. + function controllersBoundTransclude(scope, cloneAttachFn) { + var transcludeControllers; - /** - * looks up the directive and decorates it with exception handling and proper parameters. We - * call this the boundDirective. - * + // no scope passed + if (arguments.length < 2) { + cloneAttachFn = scope; + scope = undefined; + } + + if (hasElementTranscludeDirective) { + transcludeControllers = elementControllers; + } + + return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers); + } + } + } + + function markDirectivesAsIsolate(directives) { + // mark all directives as needing isolate scope. + for (var j = 0, jj = directives.length; j < jj; j++) { + directives[j] = inherit(directives[j], {$$isolateScope: true}); + } + } + + /** + * looks up the directive and decorates it with exception handling and proper parameters. We + * call this the boundDirective. + * * @param {string} name name of the directive to look up. * @param {string} location The directive must be found in specific format. * String containing any of theses characters: @@ -4425,10 +6779,12 @@ function $CompileProvider($provide) { * * `A': attribute * * `C`: class * * `M`: comment - * @returns true if directive was added. + * @returns {boolean} true if directive was added. */ - function addDirective(tDirectives, name, location, maxPriority) { - var match = false; + function addDirective(tDirectives, name, location, maxPriority, ignoreDirective, startAttrName, + endAttrName) { + if (name === ignoreDirective) return null; + var match = null; if (hasDirectives.hasOwnProperty(name)) { for(var directive, directives = $injector.get(name + Suffix), i = 0, ii = directives.length; i directive.priority) && directive.restrict.indexOf(location) != -1) { + if (startAttrName) { + directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); + } tDirectives.push(directive); - match = true; + match = directive; } } catch(e) { $exceptionHandler(e); } } @@ -4462,7 +6821,7 @@ function $CompileProvider($provide) { // reapply the old attributes to the new element forEach(dst, function(value, key) { if (key.charAt(0) != '$') { - if (src[key]) { + if (src[key] && src[key] !== value) { value += (key === 'style' ? ';' : ' ') + src[key]; } dst.$set(key, value, true, srcAttr[key]); @@ -4476,6 +6835,10 @@ function $CompileProvider($provide) { dst['class'] = (dst['class'] ? dst['class'] + ' ' : '') + value; } else if (key == 'style') { $element.attr('style', $element.attr('style') + ';' + value); + dst['style'] = (dst['style'] ? dst['style'] + ';' : '') + value; + // `dst` will never contain hasOwnProperty as DOM parser won't let it. + // You will get an "InvalidCharacterError: DOM Exception 5" error if you + // have an attribute like "has-own-property" or "data-has-own-property", etc. } else if (key.charAt(0) != '$' && !dst.hasOwnProperty(key)) { dst[key] = value; dstAttr[key] = srcAttr[key]; @@ -4484,8 +6847,8 @@ function $CompileProvider($provide) { } - function compileTemplateUrl(directives, beforeTemplateNodeLinkFn, $compileNode, tAttrs, - $rootElement, replace, childTranscludeFn) { + function compileTemplateUrl(directives, $compileNode, tAttrs, + $rootElement, childTranscludeFn, preLinkFns, postLinkFns, previousCompileContext) { var linkQueue = [], afterTemplateNodeLinkFn, afterTemplateChildLinkFn, @@ -4493,28 +6856,42 @@ function $CompileProvider($provide) { origAsyncDirective = directives.shift(), // The fact that we have to copy and patch the directive seems wrong! derivedSyncDirective = extend({}, origAsyncDirective, { - controller: null, templateUrl: null, transclude: null, scope: null - }); + templateUrl: null, transclude: null, replace: null, $$originalDirective: origAsyncDirective + }), + templateUrl = (isFunction(origAsyncDirective.templateUrl)) + ? origAsyncDirective.templateUrl($compileNode, tAttrs) + : origAsyncDirective.templateUrl; - $compileNode.html(''); + $compileNode.empty(); - $http.get(origAsyncDirective.templateUrl, {cache: $templateCache}). + $http.get($sce.getTrustedResourceUrl(templateUrl), {cache: $templateCache}). success(function(content) { - var compileNode, tempTemplateAttrs, $template; + var compileNode, tempTemplateAttrs, $template, childBoundTranscludeFn; content = denormalizeTemplate(content); - if (replace) { - $template = jqLite('
' + trim(content) + '
').contents(); + if (origAsyncDirective.replace) { + if (jqLiteIsTextNode(content)) { + $template = []; + } else { + $template = jqLite(trim(content)); + } compileNode = $template[0]; if ($template.length != 1 || compileNode.nodeType !== 1) { - throw new Error(MULTI_ROOT_TEMPLATE_ERROR + content); + throw $compileMinErr('tplrt', + "Template for directive '{0}' must have exactly one root element. {1}", + origAsyncDirective.name, templateUrl); } tempTemplateAttrs = {$attr: {}}; replaceWith($rootElement, $compileNode, compileNode); - collectDirectives(compileNode, directives, tempTemplateAttrs); + var templateDirectives = collectDirectives(compileNode, [], tempTemplateAttrs); + + if (isObject(origAsyncDirective.scope)) { + markDirectivesAsIsolate(templateDirectives); + } + directives = templateDirectives.concat(directives); mergeTemplateAttributes(tAttrs, tempTemplateAttrs); } else { compileNode = beforeTemplateCompileNode; @@ -4522,43 +6899,64 @@ function $CompileProvider($provide) { } directives.unshift(derivedSyncDirective); - afterTemplateNodeLinkFn = applyDirectivesToNode(directives, compileNode, tAttrs, childTranscludeFn); - afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes, childTranscludeFn); + afterTemplateNodeLinkFn = applyDirectivesToNode(directives, compileNode, tAttrs, + childTranscludeFn, $compileNode, origAsyncDirective, preLinkFns, postLinkFns, + previousCompileContext); + forEach($rootElement, function(node, i) { + if (node == compileNode) { + $rootElement[i] = $compileNode[0]; + } + }); + afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes, childTranscludeFn); while(linkQueue.length) { - var controller = linkQueue.pop(), - linkRootElement = linkQueue.pop(), - beforeTemplateLinkNode = linkQueue.pop(), - scope = linkQueue.pop(), - linkNode = compileNode; + var scope = linkQueue.shift(), + beforeTemplateLinkNode = linkQueue.shift(), + linkRootElement = linkQueue.shift(), + boundTranscludeFn = linkQueue.shift(), + linkNode = $compileNode[0]; if (beforeTemplateLinkNode !== beforeTemplateCompileNode) { - // it was cloned therefore we have to clone as well. - linkNode = JQLiteClone(compileNode); + var oldClasses = beforeTemplateLinkNode.className; + + if (!(previousCompileContext.hasElementTranscludeDirective && + origAsyncDirective.replace)) { + // it was cloned therefore we have to clone as well. + linkNode = jqLiteClone(compileNode); + } + replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode); - } - afterTemplateNodeLinkFn(function() { - beforeTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement, controller); - }, scope, linkNode, $rootElement, controller); + // Copy in CSS classes from original node + safeAddClass(jqLite(linkNode), oldClasses); + } + if (afterTemplateNodeLinkFn.transcludeOnThisElement) { + childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn); + } else { + childBoundTranscludeFn = boundTranscludeFn; + } + afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement, + childBoundTranscludeFn); } linkQueue = null; }). error(function(response, code, headers, config) { - throw Error('Failed to load template: ' + config.url); + throw $compileMinErr('tpload', 'Failed to load template: {0}', config.url); }); - return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, controller) { + return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) { + var childBoundTranscludeFn = boundTranscludeFn; if (linkQueue) { linkQueue.push(scope); linkQueue.push(node); linkQueue.push(rootElement); - linkQueue.push(controller); + linkQueue.push(childBoundTranscludeFn); } else { - afterTemplateNodeLinkFn(function() { - beforeTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, controller); - }, scope, node, rootElement, controller); + if (afterTemplateNodeLinkFn.transcludeOnThisElement) { + childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn); + } + afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn); } }; } @@ -4568,33 +6966,59 @@ function $CompileProvider($provide) { * Sorting function for bound directives. */ function byPriority(a, b) { - return b.priority - a.priority; + var diff = b.priority - a.priority; + if (diff !== 0) return diff; + if (a.name !== b.name) return (a.name < b.name) ? -1 : 1; + return a.index - b.index; } function assertNoDuplicate(what, previousDirective, directive, element) { if (previousDirective) { - throw Error('Multiple directives [' + previousDirective.name + ', ' + - directive.name + '] asking for ' + what + ' on: ' + startingTag(element)); + throw $compileMinErr('multidir', 'Multiple directives [{0}, {1}] asking for {2} on: {3}', + previousDirective.name, directive.name, what, startingTag(element)); } } - function addTextInterpolateDirective(directives, text) { - var interpolateFn = $interpolate(text, true); - if (interpolateFn) { - directives.push({ - priority: 0, - compile: valueFn(function textInterpolateLinkFn(scope, node) { - var parent = node.parent(), - bindings = parent.data('$binding') || []; - bindings.push(interpolateFn); - safeAddClass(parent.data('$binding', bindings), 'ng-binding'); - scope.$watch(interpolateFn, function interpolateFnWatchAction(value) { - node[0].nodeValue = value; - }); - }) - }); + function addTextInterpolateDirective(directives, text) { + var interpolateFn = $interpolate(text, true); + if (interpolateFn) { + directives.push({ + priority: 0, + compile: function textInterpolateCompileFn(templateNode) { + // when transcluding a template that has bindings in the root + // then we don't have a parent and should do this in the linkFn + var parent = templateNode.parent(), hasCompileParent = parent.length; + if (hasCompileParent) safeAddClass(templateNode.parent(), 'ng-binding'); + + return function textInterpolateLinkFn(scope, node) { + var parent = node.parent(), + bindings = parent.data('$binding') || []; + bindings.push(interpolateFn); + parent.data('$binding', bindings); + if (!hasCompileParent) safeAddClass(parent, 'ng-binding'); + scope.$watch(interpolateFn, function interpolateFnWatchAction(value) { + node[0].nodeValue = value; + }); + }; + } + }); + } + } + + + function getTrustedContext(node, attrNormalizedName) { + if (attrNormalizedName == "srcdoc") { + return $sce.HTML; + } + var tag = nodeName_(node); + // maction[xlink:href] can source SVG. It's not limited to . + if (attrNormalizedName == "xlinkHref" || + (tag == "FORM" && attrNormalizedName == "action") || + (tag != "IMG" && (attrNormalizedName == "src" || + attrNormalizedName == "ngSrc"))) { + return $sce.RESOURCE_URL; } } @@ -4606,24 +7030,54 @@ function $CompileProvider($provide) { if (!interpolateFn) return; + if (name === "multiple" && nodeName_(node) === "SELECT") { + throw $compileMinErr("selmulti", + "Binding to the 'multiple' attribute is not supported. Element: {0}", + startingTag(node)); + } + directives.push({ priority: 100, - compile: valueFn(function attrInterpolateLinkFn(scope, element, attr) { - var $$observers = (attr.$$observers || (attr.$$observers = {})); + compile: function() { + return { + pre: function attrInterpolatePreLinkFn(scope, element, attr) { + var $$observers = (attr.$$observers || (attr.$$observers = {})); + + if (EVENT_HANDLER_ATTR_REGEXP.test(name)) { + throw $compileMinErr('nodomevents', + "Interpolations for HTML DOM event attributes are disallowed. Please use the " + + "ng- versions (such as ng-click instead of onclick) instead."); + } - if (name === 'class') { - // we need to interpolate classes again, in the case the element was replaced - // and therefore the two class attrs got merged - we want to interpolate the result - interpolateFn = $interpolate(attr[name], true); + // we need to interpolate again, in case the attribute value has been updated + // (e.g. by another directive's compile function) + interpolateFn = $interpolate(attr[name], true, getTrustedContext(node, name)); + + // if attribute was updated so that there is no interpolation going on we don't want to + // register any observers + if (!interpolateFn) return; + + // TODO(i): this should likely be attr.$set(name, iterpolateFn(scope) so that we reset the + // actual attr value + attr[name] = interpolateFn(scope); + ($$observers[name] || ($$observers[name] = [])).$$inter = true; + (attr.$$observers && attr.$$observers[name].$$scope || scope). + $watch(interpolateFn, function interpolateFnWatchAction(newValue, oldValue) { + //special case for class attribute addition + removal + //so that class changes can tap into the animation + //hooks provided by the $animate service. Be sure to + //skip animations when the first digest occurs (when + //both the new and the old values are the same) since + //the CSS classes are the non-interpolated values + if(name === 'class' && newValue != oldValue) { + attr.$updateClass(newValue, oldValue); + } else { + attr.$set(name, newValue); + } + }); + } + }; } - - attr[name] = undefined; - ($$observers[name] || ($$observers[name] = [])).$$inter = true; - (attr.$$observers && attr.$$observers[name].$$scope || scope). - $watch(interpolateFn, function interpolateFnWatchAction(value) { - attr.$set(name, value); - }); - }) }); } @@ -4633,31 +7087,56 @@ function $CompileProvider($provide) { * have no parents, provided that the containing jqLite collection is provided. * * @param {JqLite=} $rootElement The root of the compile tree. Used so that we can replace nodes - * in the root of the tree. - * @param {JqLite} $element The jqLite element which we are going to replace. We keep the shell, - * but replace its DOM node reference. + * in the root of the tree. + * @param {JqLite} elementsToRemove The jqLite element which we are going to replace. We keep + * the shell, but replace its DOM node reference. * @param {Node} newNode The new DOM node. */ - function replaceWith($rootElement, $element, newNode) { - var oldNode = $element[0], - parent = oldNode.parentNode, + function replaceWith($rootElement, elementsToRemove, newNode) { + var firstElementToRemove = elementsToRemove[0], + removeCount = elementsToRemove.length, + parent = firstElementToRemove.parentNode, i, ii; if ($rootElement) { for(i = 0, ii = $rootElement.length; i < ii; i++) { - if ($rootElement[i] == oldNode) { - $rootElement[i] = newNode; + if ($rootElement[i] == firstElementToRemove) { + $rootElement[i++] = newNode; + for (var j = i, j2 = j + removeCount - 1, + jj = $rootElement.length; + j < jj; j++, j2++) { + if (j2 < jj) { + $rootElement[j] = $rootElement[j2]; + } else { + delete $rootElement[j]; + } + } + $rootElement.length -= removeCount - 1; break; } } } if (parent) { - parent.replaceChild(newNode, oldNode); + parent.replaceChild(newNode, firstElementToRemove); + } + var fragment = document.createDocumentFragment(); + fragment.appendChild(firstElementToRemove); + newNode[jqLite.expando] = firstElementToRemove[jqLite.expando]; + for (var k = 1, kk = elementsToRemove.length; k < kk; k++) { + var element = elementsToRemove[k]; + jqLite(element).remove(); // must do this way to clean up expando + fragment.appendChild(element); + delete elementsToRemove[k]; } - newNode[jqLite.expando] = oldNode[jqLite.expando]; - $element[0] = newNode; + elementsToRemove[0] = newNode; + elementsToRemove.length = 1; + } + + + function cloneAndAnnotateFn(fn, annotation) { + return extend(function() { return fn.apply(null, arguments); }, fn, annotation); } }]; } @@ -4666,7 +7145,7 @@ var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i; /** * Converts all accepted directives format into proper directive name. * All of these will become 'myDirective': - * my:DiRective + * my:Directive * my-directive * x-my-directive * data-my:directive @@ -4679,40 +7158,42 @@ function directiveNormalize(name) { } /** - * @ngdoc object - * @name ng.$compile.directive.Attributes - * @description + * @ngdoc type + * @name $compile.directive.Attributes * - * A shared object between directive compile / linking functions which contains normalized DOM element - * attributes. The the values reflect current binding state `{{ }}`. The normalization is needed - * since all of these are treated as equivalent in Angular: + * @description + * A shared object between directive compile / linking functions which contains normalized DOM + * element attributes. The values reflect current binding state `{{ }}`. The normalization is + * needed since all of these are treated as equivalent in Angular: * - * + * ``` + * + * ``` */ /** * @ngdoc property - * @name ng.$compile.directive.Attributes#$attr - * @propertyOf ng.$compile.directive.Attributes - * @returns {object} A map of DOM element attribute names to the normalized name. This is - * needed to do reverse lookup from normalized name back to actual name. + * @name $compile.directive.Attributes#$attr + * + * @description + * A map of DOM element attribute names to the normalized name. This is + * needed to do reverse lookup from normalized name back to actual name. */ /** - * @ngdoc function - * @name ng.$compile.directive.Attributes#$set - * @methodOf ng.$compile.directive.Attributes - * @function + * @ngdoc method + * @name $compile.directive.Attributes#$set + * @kind function * * @description * Set DOM element attribute value. * * * @param {string} name Normalized element attribute name of the property to modify. The name is - * revers translated using the {@link ng.$compile.directive.Attributes#$attr $attr} + * reverse-translated using the {@link ng.$compile.directive.Attributes#$attr $attr} * property to the original name. - * @param {string} value Value to set the attribute to. + * @param {string} value Value to set the attribute to. The value can be an interpolated string. */ @@ -4736,9 +7217,25 @@ function directiveLinkingFn( /* function(Function) */ boundTranscludeFn ){} +function tokenDifference(str1, str2) { + var values = '', + tokens1 = str1.split(/\s+/), + tokens2 = str2.split(/\s+/); + + outer: + for(var i = 0; i < tokens1.length; i++) { + var token = tokens1[i]; + for(var j = 0; j < tokens2.length; j++) { + if(token == tokens2[j]) continue outer; + } + values += (values.length > 0 ? ' ' : '') + token; + } + return values; +} + /** - * @ngdoc object - * @name ng.$controllerProvider + * @ngdoc provider + * @name $controllerProvider * @description * The {@link ng.$controller $controller service} is used by Angular to create new * controllers. @@ -4747,20 +7244,22 @@ function directiveLinkingFn( * {@link ng.$controllerProvider#register register} method. */ function $ControllerProvider() { - var controllers = {}; + var controllers = {}, + CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/; /** - * @ngdoc function - * @name ng.$controllerProvider#register - * @methodOf ng.$controllerProvider - * @param {string} name Controller name + * @ngdoc method + * @name $controllerProvider#register + * @param {string|Object} name Controller name, or an object map of controllers where the keys are + * the names and the values are the constructors. * @param {Function|Array} constructor Controller constructor fn (optionally decorated with DI * annotations in the array notation). */ this.register = function(name, constructor) { + assertNotHasOwnProperty(name, 'controller'); if (isObject(name)) { - extend(controllers, name) + extend(controllers, name); } else { controllers[name] = constructor; } @@ -4770,8 +7269,8 @@ function $ControllerProvider() { this.$get = ['$injector', '$window', function($injector, $window) { /** - * @ngdoc function - * @name ng.$controller + * @ngdoc service + * @name $controller * @requires $injector * * @param {Function|string} constructor If called with a function then it's considered to be the @@ -4788,33 +7287,64 @@ function $ControllerProvider() { * @description * `$controller` service is responsible for instantiating controllers. * - * It's just a simple call to {@link AUTO.$injector $injector}, but extracted into - * a service, so that one can override this service with {@link https://gist.github.com/1649788 - * BC version}. + * It's just a simple call to {@link auto.$injector $injector}, but extracted into + * a service, so that one can override this service with [BC version](https://gist.github.com/1649788). */ - return function(constructor, locals) { - if(isString(constructor)) { - var name = constructor; - constructor = controllers.hasOwnProperty(name) - ? controllers[name] - : getter(locals.$scope, name, true) || getter($window, name, true); + return function(expression, locals) { + var instance, match, constructor, identifier; + + if(isString(expression)) { + match = expression.match(CNTRL_REG), + constructor = match[1], + identifier = match[3]; + expression = controllers.hasOwnProperty(constructor) + ? controllers[constructor] + : getter(locals.$scope, constructor, true) || getter($window, constructor, true); + + assertArgFn(expression, constructor, true); + } + + instance = $injector.instantiate(expression, locals); + + if (identifier) { + if (!(locals && typeof locals.$scope === 'object')) { + throw minErr('$controller')('noscp', + "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.", + constructor || expression.name, identifier); + } - assertArgFn(constructor, name, true); + locals.$scope[identifier] = instance; } - return $injector.instantiate(constructor, locals); + return instance; }; }]; } /** - * @ngdoc object - * @name ng.$document + * @ngdoc service + * @name $document * @requires $window * * @description - * A {@link angular.element jQuery (lite)}-wrapped reference to the browser's `window.document` - * element. + * A {@link angular.element jQuery or jqLite} wrapper for the browser's `window.document` object. + * + * @example + + +
+

$document title:

+

window.document title:

+
+
+ + angular.module('documentExample', []) + .controller('ExampleController', ['$scope', '$document', function($scope, $document) { + $scope.title = $document[0].title; + $scope.windowTitle = angular.element(window.document)[0].title; + }]); + +
*/ function $DocumentProvider(){ this.$get = ['$window', function(window){ @@ -4823,9 +7353,9 @@ function $DocumentProvider(){ } /** - * @ngdoc function - * @name ng.$exceptionHandler - * @requires $log + * @ngdoc service + * @name $exceptionHandler + * @requires ng.$log * * @description * Any uncaught exception in angular expressions is delegated to this service. @@ -4835,6 +7365,20 @@ function $DocumentProvider(){ * In unit tests, if `angular-mocks.js` is loaded, this service is overridden by * {@link ngMock.$exceptionHandler mock $exceptionHandler} which aids in testing. * + * ## Example: + * + * ```js + * angular.module('exceptionOverride', []).factory('$exceptionHandler', function () { + * return function (exception, cause) { + * exception.message += ' (caused by "' + cause + '")'; + * throw exception; + * }; + * }); + * ``` + * + * This example will override the normal action of `$exceptionHandler`, to make angular + * exceptions fail hard when they happen, instead of just logging to the console. + * * @param {Error} exception Exception associated with the error. * @param {string=} cause optional information about the context in which * the error was thrown. @@ -4849,456 +7393,2161 @@ function $ExceptionHandlerProvider() { } /** - * @ngdoc object - * @name ng.$interpolateProvider - * @function + * Parse headers into key value object * - * @description + * @param {string} headers Raw headers as a string + * @returns {Object} Parsed headers as key value object + */ +function parseHeaders(headers) { + var parsed = {}, key, val, i; + + if (!headers) return parsed; + + forEach(headers.split('\n'), function(line) { + i = line.indexOf(':'); + key = lowercase(trim(line.substr(0, i))); + val = trim(line.substr(i + 1)); + + if (key) { + parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; + } + }); + + return parsed; +} + + +/** + * Returns a function that provides access to parsed headers. * - * Used for configuring the interpolation markup. Defaults to `{{` and `}}`. + * Headers are lazy parsed when first requested. + * @see parseHeaders + * + * @param {(string|Object)} headers Headers to provide access to. + * @returns {function(string=)} Returns a getter function which if called with: + * + * - if called with single an argument returns a single header value or null + * - if called with no arguments returns an object containing all headers. */ -function $InterpolateProvider() { - var startSymbol = '{{'; - var endSymbol = '}}'; +function headersGetter(headers) { + var headersObj = isObject(headers) ? headers : undefined; + + return function(name) { + if (!headersObj) headersObj = parseHeaders(headers); + + if (name) { + return headersObj[lowercase(name)] || null; + } + + return headersObj; + }; +} + + +/** + * Chain all given functions + * + * This function is used for both request and response transforming + * + * @param {*} data Data to transform. + * @param {function(string=)} headers Http headers getter fn. + * @param {(Function|Array.)} fns Function or an array of functions. + * @returns {*} Transformed data. + */ +function transformData(data, headers, fns) { + if (isFunction(fns)) + return fns(data, headers); + + forEach(fns, function(fn) { + data = fn(data, headers); + }); + + return data; +} + + +function isSuccess(status) { + return 200 <= status && status < 300; +} + + +/** + * @ngdoc provider + * @name $httpProvider + * @description + * Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service. + * */ +function $HttpProvider() { + var JSON_START = /^\s*(\[|\{[^\{])/, + JSON_END = /[\}\]]\s*$/, + PROTECTION_PREFIX = /^\)\]\}',?\n/, + CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': 'application/json;charset=utf-8'}; /** - * @ngdoc method - * @name ng.$interpolateProvider#startSymbol - * @methodOf ng.$interpolateProvider + * @ngdoc property + * @name $httpProvider#defaults * @description - * Symbol to denote start of expression in the interpolated string. Defaults to `{{`. * - * @param {string=} value new value to set the starting symbol to. - * @returns {string|self} Returns the symbol when used as getter and self if used as setter. - */ - this.startSymbol = function(value){ - if (value) { - startSymbol = value; - return this; - } else { - return startSymbol; - } + * Object containing default values for all {@link ng.$http $http} requests. + * + * - **`defaults.xsrfCookieName`** - {string} - Name of cookie containing the XSRF token. + * Defaults value is `'XSRF-TOKEN'`. + * + * - **`defaults.xsrfHeaderName`** - {string} - Name of HTTP header to populate with the + * XSRF token. Defaults value is `'X-XSRF-TOKEN'`. + * + * - **`defaults.headers`** - {Object} - Default headers for all $http requests. + * Refer to {@link ng.$http#setting-http-headers $http} for documentation on + * setting default headers. + * - **`defaults.headers.common`** + * - **`defaults.headers.post`** + * - **`defaults.headers.put`** + * - **`defaults.headers.patch`** + **/ + var defaults = this.defaults = { + // transform incoming response data + transformResponse: [function(data) { + if (isString(data)) { + // strip json vulnerability protection prefix + data = data.replace(PROTECTION_PREFIX, ''); + if (JSON_START.test(data) && JSON_END.test(data)) + data = fromJson(data); + } + return data; + }], + + // transform outgoing request data + transformRequest: [function(d) { + return isObject(d) && !isFile(d) && !isBlob(d) ? toJson(d) : d; + }], + + // default headers + headers: { + common: { + 'Accept': 'application/json, text/plain, */*' + }, + post: shallowCopy(CONTENT_TYPE_APPLICATION_JSON), + put: shallowCopy(CONTENT_TYPE_APPLICATION_JSON), + patch: shallowCopy(CONTENT_TYPE_APPLICATION_JSON) + }, + + xsrfCookieName: 'XSRF-TOKEN', + xsrfHeaderName: 'X-XSRF-TOKEN' }; /** - * @ngdoc method - * @name ng.$interpolateProvider#endSymbol - * @methodOf ng.$interpolateProvider + * @ngdoc property + * @name $httpProvider#interceptors * @description - * Symbol to denote the end of expression in the interpolated string. Defaults to `}}`. * - * @param {string=} value new value to set the ending symbol to. - * @returns {string|self} Returns the symbol when used as getter and self if used as setter. + * Array containing service factories for all synchronous or asynchronous {@link ng.$http $http} + * pre-processing of request or postprocessing of responses. + * + * These service factories are ordered by request, i.e. they are applied in the same order as the + * array, on request, but reverse order, on response. + * + * {@link ng.$http#interceptors Interceptors detailed info} + **/ + var interceptorFactories = this.interceptors = []; + + /** + * For historical reasons, response interceptors are ordered by the order in which + * they are applied to the response. (This is the opposite of interceptorFactories) */ - this.endSymbol = function(value){ - if (value) { - endSymbol = value; - return this; - } else { - return endSymbol; - } - }; + var responseInterceptorFactories = this.responseInterceptors = []; + this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', + function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { - this.$get = ['$parse', function($parse) { - var startSymbolLength = startSymbol.length, - endSymbolLength = endSymbol.length; + var defaultCache = $cacheFactory('$http'); /** - * @ngdoc function - * @name ng.$interpolate - * @function - * - * @requires $parse - * - * @description - * - * Compiles a string with markup into an interpolation function. This service is used by the - * HTML {@link ng.$compile $compile} service for data binding. See - * {@link ng.$interpolateProvider $interpolateProvider} for configuring the - * interpolation markup. - * - * -
-         var $interpolate = ...; // injected
-         var exp = $interpolate('Hello {{name}}!');
-         expect(exp({name:'Angular'}).toEqual('Hello Angular!');
-       
- * - * - * @param {string} text The text with markup to interpolate. - * @param {boolean=} mustHaveExpression if set to true then the interpolation string must have - * embedded expression in order to return an interpolation function. Strings with no - * embedded expression will return null for the interpolation function. - * @returns {function(context)} an interpolation function which is used to compute the interpolated - * string. The function has these parameters: - * - * * `context`: an object against which any expressions embedded in the strings are evaluated - * against. - * + * Interceptors stored in reverse order. Inner interceptors before outer interceptors. + * The reversal is needed so that we can build up the interception chain around the + * server request. */ - function $interpolate(text, mustHaveExpression) { - var startIndex, - endIndex, - index = 0, - parts = [], - length = text.length, - hasInterpolation = false, - fn, - exp, - concat = []; + var reversedInterceptors = []; - while(index < length) { - if ( ((startIndex = text.indexOf(startSymbol, index)) != -1) && - ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) ) { - (index != startIndex) && parts.push(text.substring(index, startIndex)); - parts.push(fn = $parse(exp = text.substring(startIndex + startSymbolLength, endIndex))); - fn.exp = exp; - index = endIndex + endSymbolLength; - hasInterpolation = true; - } else { - // we did not find anything, so we have to add the remainder to the parts array - (index != length) && parts.push(text.substring(index)); - index = length; - } - } + forEach(interceptorFactories, function(interceptorFactory) { + reversedInterceptors.unshift(isString(interceptorFactory) + ? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory)); + }); - if (!(length = parts.length)) { - // we added, nothing, must have been an empty string. - parts.push(''); - length = 1; - } + forEach(responseInterceptorFactories, function(interceptorFactory, index) { + var responseFn = isString(interceptorFactory) + ? $injector.get(interceptorFactory) + : $injector.invoke(interceptorFactory); - if (!mustHaveExpression || hasInterpolation) { - concat.length = length; - fn = function(context) { - for(var i = 0, ii = length, part; i html5 url - } else { - return composeProtocolHostPort(match.protocol, match.host, match.port) + - pathPrefixFromBase(basePath) + match.hash.substr(hashPrefix.length); - } -} - - -function convertToHashbangUrl(url, basePath, hashPrefix) { - var match = matchUrl(url); - - // already hashbang url - if (decodeURIComponent(match.path) == basePath && !isUndefined(match.hash) && - match.hash.indexOf(hashPrefix) === 0) { - return url; - // convert html5 url -> hashbang url - } else { - var search = match.search && '?' + match.search || '', - hash = match.hash && '#' + match.hash || '', - pathPrefix = pathPrefixFromBase(basePath), - path = match.path.substr(pathPrefix.length); - - if (match.path.indexOf(pathPrefix) !== 0) { - throw Error('Invalid url "' + url + '", missing path prefix "' + pathPrefix + '" !'); - } - - return composeProtocolHostPort(match.protocol, match.host, match.port) + basePath + - '#' + hashPrefix + path + search + hash; - } -} - - -/** - * LocationUrl represents an url - * This object is exposed as $location service when HTML5 mode is enabled and supported - * - * @constructor - * @param {string} url HTML5 url - * @param {string} pathPrefix - */ -function LocationUrl(url, pathPrefix, appBaseUrl) { - pathPrefix = pathPrefix || ''; - - /** - * Parse given html5 (regular) url string into properties - * @param {string} newAbsoluteUrl HTML5 url - * @private - */ - this.$$parse = function(newAbsoluteUrl) { - var match = matchUrl(newAbsoluteUrl, this); - - if (match.path.indexOf(pathPrefix) !== 0) { - throw Error('Invalid url "' + newAbsoluteUrl + '", missing path prefix "' + pathPrefix + '" !'); - } - - this.$$path = decodeURIComponent(match.path.substr(pathPrefix.length)); - this.$$search = parseKeyValue(match.search); - this.$$hash = match.hash && decodeURIComponent(match.hash) || ''; - - this.$$compose(); - }; - - /** - * Compose url and update `absUrl` property - * @private - */ - this.$$compose = function() { - var search = toKeyValue(this.$$search), - hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; - - this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; - this.$$absUrl = composeProtocolHostPort(this.$$protocol, this.$$host, this.$$port) + - pathPrefix + this.$$url; - }; - - - this.$$rewriteAppUrl = function(absoluteLinkUrl) { - if(absoluteLinkUrl.indexOf(appBaseUrl) == 0) { - return absoluteLinkUrl; - } - } - - - this.$$parse(url); -} - - -/** - * LocationHashbangUrl represents url - * This object is exposed as $location service when html5 history api is disabled or not supported - * - * @constructor - * @param {string} url Legacy url - * @param {string} hashPrefix Prefix for hash part (containing path and search) - */ -function LocationHashbangUrl(url, hashPrefix, appBaseUrl) { - var basePath; - - /** - * Parse given hashbang url into properties - * @param {string} url Hashbang url - * @private - */ - this.$$parse = function(url) { - var match = matchUrl(url, this); - - - if (match.hash && match.hash.indexOf(hashPrefix) !== 0) { - throw Error('Invalid url "' + url + '", missing hash prefix "' + hashPrefix + '" !'); - } - - basePath = match.path + (match.search ? '?' + match.search : ''); - match = HASH_MATCH.exec((match.hash || '').substr(hashPrefix.length)); - if (match[1]) { - this.$$path = (match[1].charAt(0) == '/' ? '' : '/') + decodeURIComponent(match[1]); - } else { - this.$$path = ''; - } - - this.$$search = parseKeyValue(match[3]); - this.$$hash = match[5] && decodeURIComponent(match[5]) || ''; - - this.$$compose(); - }; - - /** - * Compose hashbang url and update `absUrl` property - * @private - */ - this.$$compose = function() { - var search = toKeyValue(this.$$search), - hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; - - this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; - this.$$absUrl = composeProtocolHostPort(this.$$protocol, this.$$host, this.$$port) + - basePath + (this.$$url ? '#' + hashPrefix + this.$$url : ''); - }; - - this.$$rewriteAppUrl = function(absoluteLinkUrl) { - if(absoluteLinkUrl.indexOf(appBaseUrl) == 0) { - return absoluteLinkUrl; - } - } - - - this.$$parse(url); -} - - -LocationUrl.prototype = { - - /** - * Has any change been replacing ? - * @private - */ - $$replace: false, - - /** - * @ngdoc method - * @name ng.$location#absUrl - * @methodOf ng.$location - * - * @description - * This method is getter only. - * - * Return full url representation with all segments encoded according to rules specified in - * {@link http://www.ietf.org/rfc/rfc3986.txt RFC 3986}. - * - * @return {string} full url - */ - absUrl: locationGetter('$$absUrl'), - - /** - * @ngdoc method - * @name ng.$location#url - * @methodOf ng.$location - * - * @description - * This method is getter / setter. - * - * Return url (e.g. `/path?a=b#hash`) when called without any parameter. - * - * Change path, search and hash, when called with parameter and return `$location`. - * - * @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`) - * @return {string} url - */ - url: function(url, replace) { - if (isUndefined(url)) - return this.$$url; - + * + * # General usage + * The `$http` service is a function which takes a single argument — a configuration object — + * that is used to generate an HTTP request and returns a {@link ng.$q promise} + * with two $http specific methods: `success` and `error`. + * + * ```js + * $http({method: 'GET', url: '/someUrl'}). + * success(function(data, status, headers, config) { + * // this callback will be called asynchronously + * // when the response is available + * }). + * error(function(data, status, headers, config) { + * // called asynchronously if an error occurs + * // or server returns response with an error status. + * }); + * ``` + * + * Since the returned value of calling the $http function is a `promise`, you can also use + * the `then` method to register callbacks, and these callbacks will receive a single argument – + * an object representing the response. See the API signature and type info below for more + * details. + * + * A response status code between 200 and 299 is considered a success status and + * will result in the success callback being called. Note that if the response is a redirect, + * XMLHttpRequest will transparently follow it, meaning that the error callback will not be + * called for such responses. + * + * # Writing Unit Tests that use $http + * When unit testing (using {@link ngMock ngMock}), it is necessary to call + * {@link ngMock.$httpBackend#flush $httpBackend.flush()} to flush each pending + * request using trained responses. + * + * ``` + * $httpBackend.expectGET(...); + * $http.get(...); + * $httpBackend.flush(); + * ``` + * + * # Shortcut methods + * + * Shortcut methods are also available. All shortcut methods require passing in the URL, and + * request data must be passed in for POST/PUT requests. + * + * ```js + * $http.get('/someUrl').success(successCallback); + * $http.post('/someUrl', data).success(successCallback); + * ``` + * + * Complete list of shortcut methods: + * + * - {@link ng.$http#get $http.get} + * - {@link ng.$http#head $http.head} + * - {@link ng.$http#post $http.post} + * - {@link ng.$http#put $http.put} + * - {@link ng.$http#delete $http.delete} + * - {@link ng.$http#jsonp $http.jsonp} + * - {@link ng.$http#patch $http.patch} + * + * + * # Setting HTTP Headers + * + * The $http service will automatically add certain HTTP headers to all requests. These defaults + * can be fully configured by accessing the `$httpProvider.defaults.headers` configuration + * object, which currently contains this default configuration: + * + * - `$httpProvider.defaults.headers.common` (headers that are common for all requests): + * - `Accept: application/json, text/plain, * / *` + * - `$httpProvider.defaults.headers.post`: (header defaults for POST requests) + * - `Content-Type: application/json` + * - `$httpProvider.defaults.headers.put` (header defaults for PUT requests) + * - `Content-Type: application/json` + * + * To add or overwrite these defaults, simply add or remove a property from these configuration + * objects. To add headers for an HTTP method other than POST or PUT, simply add a new object + * with the lowercased HTTP method name as the key, e.g. + * `$httpProvider.defaults.headers.get = { 'My-Header' : 'value' }. + * + * The defaults can also be set at runtime via the `$http.defaults` object in the same + * fashion. For example: + * + * ``` + * module.run(function($http) { + * $http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w' + * }); + * ``` + * + * In addition, you can supply a `headers` property in the config object passed when + * calling `$http(config)`, which overrides the defaults without changing them globally. + * + * + * # Transforming Requests and Responses + * + * Both requests and responses can be transformed using transform functions. By default, Angular + * applies these transformations: + * + * Request transformations: + * + * - If the `data` property of the request configuration object contains an object, serialize it + * into JSON format. + * + * Response transformations: + * + * - If XSRF prefix is detected, strip it (see Security Considerations section below). + * - If JSON response is detected, deserialize it using a JSON parser. + * + * To globally augment or override the default transforms, modify the + * `$httpProvider.defaults.transformRequest` and `$httpProvider.defaults.transformResponse` + * properties. These properties are by default an array of transform functions, which allows you + * to `push` or `unshift` a new transformation function into the transformation chain. You can + * also decide to completely override any default transformations by assigning your + * transformation functions to these properties directly without the array wrapper. These defaults + * are again available on the $http factory at run-time, which may be useful if you have run-time + * services you wish to be involved in your transformations. + * + * Similarly, to locally override the request/response transforms, augment the + * `transformRequest` and/or `transformResponse` properties of the configuration object passed + * into `$http`. + * + * + * # Caching + * + * To enable caching, set the request configuration `cache` property to `true` (to use default + * cache) or to a custom cache object (built with {@link ng.$cacheFactory `$cacheFactory`}). + * When the cache is enabled, `$http` stores the response from the server in the specified + * cache. The next time the same request is made, the response is served from the cache without + * sending a request to the server. + * + * Note that even if the response is served from cache, delivery of the data is asynchronous in + * the same way that real requests are. + * + * If there are multiple GET requests for the same URL that should be cached using the same + * cache, but the cache is not populated yet, only one request to the server will be made and + * the remaining requests will be fulfilled using the response from the first request. + * + * You can change the default cache to a new object (built with + * {@link ng.$cacheFactory `$cacheFactory`}) by updating the + * {@link ng.$http#properties_defaults `$http.defaults.cache`} property. All requests who set + * their `cache` property to `true` will now use this cache object. + * + * If you set the default cache to `false` then only requests that specify their own custom + * cache object will be cached. + * + * # Interceptors + * + * Before you start creating interceptors, be sure to understand the + * {@link ng.$q $q and deferred/promise APIs}. + * + * For purposes of global error handling, authentication, or any kind of synchronous or + * asynchronous pre-processing of request or postprocessing of responses, it is desirable to be + * able to intercept requests before they are handed to the server and + * responses before they are handed over to the application code that + * initiated these requests. The interceptors leverage the {@link ng.$q + * promise APIs} to fulfill this need for both synchronous and asynchronous pre-processing. + * + * The interceptors are service factories that are registered with the `$httpProvider` by + * adding them to the `$httpProvider.interceptors` array. The factory is called and + * injected with dependencies (if specified) and returns the interceptor. + * + * There are two kinds of interceptors (and two kinds of rejection interceptors): + * + * * `request`: interceptors get called with a http `config` object. The function is free to + * modify the `config` object or create a new one. The function needs to return the `config` + * object directly, or a promise containing the `config` or a new `config` object. + * * `requestError`: interceptor gets called when a previous interceptor threw an error or + * resolved with a rejection. + * * `response`: interceptors get called with http `response` object. The function is free to + * modify the `response` object or create a new one. The function needs to return the `response` + * object directly, or as a promise containing the `response` or a new `response` object. + * * `responseError`: interceptor gets called when a previous interceptor threw an error or + * resolved with a rejection. + * + * + * ```js + * // register the interceptor as a service + * $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) { + * return { + * // optional method + * 'request': function(config) { + * // do something on success + * return config; + * }, + * + * // optional method + * 'requestError': function(rejection) { + * // do something on error + * if (canRecover(rejection)) { + * return responseOrNewPromise + * } + * return $q.reject(rejection); + * }, + * + * + * + * // optional method + * 'response': function(response) { + * // do something on success + * return response; + * }, + * + * // optional method + * 'responseError': function(rejection) { + * // do something on error + * if (canRecover(rejection)) { + * return responseOrNewPromise + * } + * return $q.reject(rejection); + * } + * }; + * }); + * + * $httpProvider.interceptors.push('myHttpInterceptor'); + * + * + * // alternatively, register the interceptor via an anonymous factory + * $httpProvider.interceptors.push(function($q, dependency1, dependency2) { + * return { + * 'request': function(config) { + * // same as above + * }, + * + * 'response': function(response) { + * // same as above + * } + * }; + * }); + * ``` + * + * # Response interceptors (DEPRECATED) + * + * Before you start creating interceptors, be sure to understand the + * {@link ng.$q $q and deferred/promise APIs}. + * + * For purposes of global error handling, authentication or any kind of synchronous or + * asynchronous preprocessing of received responses, it is desirable to be able to intercept + * responses for http requests before they are handed over to the application code that + * initiated these requests. The response interceptors leverage the {@link ng.$q + * promise apis} to fulfil this need for both synchronous and asynchronous preprocessing. + * + * The interceptors are service factories that are registered with the $httpProvider by + * adding them to the `$httpProvider.responseInterceptors` array. The factory is called and + * injected with dependencies (if specified) and returns the interceptor — a function that + * takes a {@link ng.$q promise} and returns the original or a new promise. + * + * ```js + * // register the interceptor as a service + * $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) { + * return function(promise) { + * return promise.then(function(response) { + * // do something on success + * return response; + * }, function(response) { + * // do something on error + * if (canRecover(response)) { + * return responseOrNewPromise + * } + * return $q.reject(response); + * }); + * } + * }); + * + * $httpProvider.responseInterceptors.push('myHttpInterceptor'); + * + * + * // register the interceptor via an anonymous factory + * $httpProvider.responseInterceptors.push(function($q, dependency1, dependency2) { + * return function(promise) { + * // same as above + * } + * }); + * ``` + * + * + * # Security Considerations + * + * When designing web applications, consider security threats from: + * + * - [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx) + * - [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) + * + * Both server and the client must cooperate in order to eliminate these threats. Angular comes + * pre-configured with strategies that address these issues, but for this to work backend server + * cooperation is required. + * + * ## JSON Vulnerability Protection + * + * A [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx) + * allows third party website to turn your JSON resource URL into + * [JSONP](http://en.wikipedia.org/wiki/JSONP) request under some conditions. To + * counter this your server can prefix all JSON requests with following string `")]}',\n"`. + * Angular will automatically strip the prefix before processing it as JSON. + * + * For example if your server needs to return: + * ```js + * ['one','two'] + * ``` + * + * which is vulnerable to attack, your server can return: + * ```js + * )]}', + * ['one','two'] + * ``` + * + * Angular will strip the prefix, before processing the JSON. + * + * + * ## Cross Site Request Forgery (XSRF) Protection + * + * [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) is a technique by which + * an unauthorized site can gain your user's private data. Angular provides a mechanism + * to counter XSRF. When performing XHR requests, the $http service reads a token from a cookie + * (by default, `XSRF-TOKEN`) and sets it as an HTTP header (`X-XSRF-TOKEN`). Since only + * JavaScript that runs on your domain could read the cookie, your server can be assured that + * the XHR came from JavaScript running on your domain. The header will not be set for + * cross-domain requests. + * + * To take advantage of this, your server needs to set a token in a JavaScript readable session + * cookie called `XSRF-TOKEN` on the first HTTP GET request. On subsequent XHR requests the + * server can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure + * that only JavaScript running on your domain could have sent the request. The token must be + * unique for each user and must be verifiable by the server (to prevent the JavaScript from + * making up its own tokens). We recommend that the token is a digest of your site's + * authentication cookie with a [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) + * for added security. + * + * The name of the headers can be specified using the xsrfHeaderName and xsrfCookieName + * properties of either $httpProvider.defaults at config-time, $http.defaults at run-time, + * or the per-request config object. + * + * + * @param {object} config Object describing the request to be made and how it should be + * processed. The object has following properties: + * + * - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) + * - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. + * - **params** – `{Object.}` – Map of strings or objects which will be turned + * to `?key1=value1&key2=value2` after the url. If the value is not a string, it will be + * JSONified. + * - **data** – `{string|Object}` – Data to be sent as the request message data. + * - **headers** – `{Object}` – Map of strings or functions which return strings representing + * HTTP headers to send to the server. If the return value of a function is null, the + * header will not be sent. + * - **xsrfHeaderName** – `{string}` – Name of HTTP header to populate with the XSRF token. + * - **xsrfCookieName** – `{string}` – Name of cookie containing the XSRF token. + * - **transformRequest** – + * `{function(data, headersGetter)|Array.}` – + * transform function or an array of such functions. The transform function takes the http + * request body and headers and returns its transformed (typically serialized) version. + * - **transformResponse** – + * `{function(data, headersGetter)|Array.}` – + * transform function or an array of such functions. The transform function takes the http + * response body and headers and returns its transformed (typically deserialized) version. + * - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the + * GET request, otherwise if a cache instance built with + * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for + * caching. + * - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} + * that should abort the request when resolved. + * - **withCredentials** - `{boolean}` - whether to set the `withCredentials` flag on the + * XHR object. See [requests with credentials](https://developer.mozilla.org/docs/Web/HTTP/Access_control_CORS#Requests_with_credentials) + * for more information. + * - **responseType** - `{string}` - see + * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType). + * + * @returns {HttpPromise} Returns a {@link ng.$q promise} object with the + * standard `then` method and two http specific methods: `success` and `error`. The `then` + * method takes two arguments a success and an error callback which will be called with a + * response object. The `success` and `error` methods take a single argument - a function that + * will be called when the request succeeds or fails respectively. The arguments passed into + * these functions are destructured representation of the response object passed into the + * `then` method. The response object has these properties: + * + * - **data** – `{string|Object}` – The response body transformed with the transform + * functions. + * - **status** – `{number}` – HTTP status code of the response. + * - **headers** – `{function([headerName])}` – Header getter function. + * - **config** – `{Object}` – The configuration object that was used to generate the request. + * - **statusText** – `{string}` – HTTP status text of the response. + * + * @property {Array.} pendingRequests Array of config objects for currently pending + * requests. This is primarily meant to be used for debugging purposes. + * + * + * @example + + +
+ + +
+ + + +
http status code: {{status}}
+
http response data: {{data}}
+
+
+ + angular.module('httpExample', []) + .controller('FetchController', ['$scope', '$http', '$templateCache', + function($scope, $http, $templateCache) { + $scope.method = 'GET'; + $scope.url = 'http-hello.html'; + + $scope.fetch = function() { + $scope.code = null; + $scope.response = null; + + $http({method: $scope.method, url: $scope.url, cache: $templateCache}). + success(function(data, status) { + $scope.status = status; + $scope.data = data; + }). + error(function(data, status) { + $scope.data = data || "Request failed"; + $scope.status = status; + }); + }; + + $scope.updateModel = function(method, url) { + $scope.method = method; + $scope.url = url; + }; + }]); + + + Hello, $http! + + + var status = element(by.binding('status')); + var data = element(by.binding('data')); + var fetchBtn = element(by.id('fetchbtn')); + var sampleGetBtn = element(by.id('samplegetbtn')); + var sampleJsonpBtn = element(by.id('samplejsonpbtn')); + var invalidJsonpBtn = element(by.id('invalidjsonpbtn')); + + it('should make an xhr GET request', function() { + sampleGetBtn.click(); + fetchBtn.click(); + expect(status.getText()).toMatch('200'); + expect(data.getText()).toMatch(/Hello, \$http!/); + }); + +// Commented out due to flakes. See https://github.com/angular/angular.js/issues/9185 +// it('should make a JSONP request to angularjs.org', function() { +// sampleJsonpBtn.click(); +// fetchBtn.click(); +// expect(status.getText()).toMatch('200'); +// expect(data.getText()).toMatch(/Super Hero!/); +// }); + + it('should make JSONP request to invalid URL and invoke the error handler', + function() { + invalidJsonpBtn.click(); + fetchBtn.click(); + expect(status.getText()).toMatch('0'); + expect(data.getText()).toMatch('Request failed'); + }); + +
+ */ + function $http(requestConfig) { + var config = { + method: 'get', + transformRequest: defaults.transformRequest, + transformResponse: defaults.transformResponse + }; + var headers = mergeHeaders(requestConfig); + + extend(config, requestConfig); + config.headers = headers; + config.method = uppercase(config.method); + + var serverRequest = function(config) { + headers = config.headers; + var reqData = transformData(config.data, headersGetter(headers), config.transformRequest); + + // strip content-type if data is undefined + if (isUndefined(reqData)) { + forEach(headers, function(value, header) { + if (lowercase(header) === 'content-type') { + delete headers[header]; + } + }); + } + + if (isUndefined(config.withCredentials) && !isUndefined(defaults.withCredentials)) { + config.withCredentials = defaults.withCredentials; + } + + // send request + return sendReq(config, reqData, headers).then(transformResponse, transformResponse); + }; + + var chain = [serverRequest, undefined]; + var promise = $q.when(config); + + // apply interceptors + forEach(reversedInterceptors, function(interceptor) { + if (interceptor.request || interceptor.requestError) { + chain.unshift(interceptor.request, interceptor.requestError); + } + if (interceptor.response || interceptor.responseError) { + chain.push(interceptor.response, interceptor.responseError); + } + }); + + while(chain.length) { + var thenFn = chain.shift(); + var rejectFn = chain.shift(); + + promise = promise.then(thenFn, rejectFn); + } + + promise.success = function(fn) { + promise.then(function(response) { + fn(response.data, response.status, response.headers, config); + }); + return promise; + }; + + promise.error = function(fn) { + promise.then(null, function(response) { + fn(response.data, response.status, response.headers, config); + }); + return promise; + }; + + return promise; + + function transformResponse(response) { + // make a copy since the response must be cacheable + var resp = extend({}, response, { + data: transformData(response.data, response.headers, config.transformResponse) + }); + return (isSuccess(response.status)) + ? resp + : $q.reject(resp); + } + + function mergeHeaders(config) { + var defHeaders = defaults.headers, + reqHeaders = extend({}, config.headers), + defHeaderName, lowercaseDefHeaderName, reqHeaderName; + + defHeaders = extend({}, defHeaders.common, defHeaders[lowercase(config.method)]); + + // using for-in instead of forEach to avoid unecessary iteration after header has been found + defaultHeadersIteration: + for (defHeaderName in defHeaders) { + lowercaseDefHeaderName = lowercase(defHeaderName); + + for (reqHeaderName in reqHeaders) { + if (lowercase(reqHeaderName) === lowercaseDefHeaderName) { + continue defaultHeadersIteration; + } + } + + reqHeaders[defHeaderName] = defHeaders[defHeaderName]; + } + + // execute if header value is a function for merged headers + execHeaders(reqHeaders); + return reqHeaders; + + function execHeaders(headers) { + var headerContent; + + forEach(headers, function(headerFn, header) { + if (isFunction(headerFn)) { + headerContent = headerFn(); + if (headerContent != null) { + headers[header] = headerContent; + } else { + delete headers[header]; + } + } + }); + } + } + } + + $http.pendingRequests = []; + + /** + * @ngdoc method + * @name $http#get + * + * @description + * Shortcut method to perform `GET` request. + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + + /** + * @ngdoc method + * @name $http#delete + * + * @description + * Shortcut method to perform `DELETE` request. + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + + /** + * @ngdoc method + * @name $http#head + * + * @description + * Shortcut method to perform `HEAD` request. + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + + /** + * @ngdoc method + * @name $http#jsonp + * + * @description + * Shortcut method to perform `JSONP` request. + * + * @param {string} url Relative or absolute URL specifying the destination of the request. + * The name of the callback should be the string `JSON_CALLBACK`. + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + createShortMethods('get', 'delete', 'head', 'jsonp'); + + /** + * @ngdoc method + * @name $http#post + * + * @description + * Shortcut method to perform `POST` request. + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {*} data Request content + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + + /** + * @ngdoc method + * @name $http#put + * + * @description + * Shortcut method to perform `PUT` request. + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {*} data Request content + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + + /** + * @ngdoc method + * @name $http#patch + * + * @description + * Shortcut method to perform `PATCH` request. + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {*} data Request content + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + createShortMethodsWithData('post', 'put', 'patch'); + + /** + * @ngdoc property + * @name $http#defaults + * + * @description + * Runtime equivalent of the `$httpProvider.defaults` property. Allows configuration of + * default headers, withCredentials as well as request and response transformations. + * + * See "Setting HTTP Headers" and "Transforming Requests and Responses" sections above. + */ + $http.defaults = defaults; + + + return $http; + + + function createShortMethods(names) { + forEach(arguments, function(name) { + $http[name] = function(url, config) { + return $http(extend(config || {}, { + method: name, + url: url + })); + }; + }); + } + + + function createShortMethodsWithData(name) { + forEach(arguments, function(name) { + $http[name] = function(url, data, config) { + return $http(extend(config || {}, { + method: name, + url: url, + data: data + })); + }; + }); + } + + + /** + * Makes the request. + * + * !!! ACCESSES CLOSURE VARS: + * $httpBackend, defaults, $log, $rootScope, defaultCache, $http.pendingRequests + */ + function sendReq(config, reqData, reqHeaders) { + var deferred = $q.defer(), + promise = deferred.promise, + cache, + cachedResp, + url = buildUrl(config.url, config.params); + + $http.pendingRequests.push(config); + promise.then(removePendingReq, removePendingReq); + + + if ((config.cache || defaults.cache) && config.cache !== false && + (config.method === 'GET' || config.method === 'JSONP')) { + cache = isObject(config.cache) ? config.cache + : isObject(defaults.cache) ? defaults.cache + : defaultCache; + } + + if (cache) { + cachedResp = cache.get(url); + if (isDefined(cachedResp)) { + if (isPromiseLike(cachedResp)) { + // cached request has already been sent, but there is no response yet + cachedResp.then(removePendingReq, removePendingReq); + return cachedResp; + } else { + // serving from cache + if (isArray(cachedResp)) { + resolvePromise(cachedResp[1], cachedResp[0], shallowCopy(cachedResp[2]), cachedResp[3]); + } else { + resolvePromise(cachedResp, 200, {}, 'OK'); + } + } + } else { + // put the promise for the non-transformed response into cache as a placeholder + cache.put(url, promise); + } + } + + + // if we won't have the response in cache, set the xsrf headers and + // send the request to the backend + if (isUndefined(cachedResp)) { + var xsrfValue = urlIsSameOrigin(config.url) + ? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName] + : undefined; + if (xsrfValue) { + reqHeaders[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue; + } + + $httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout, + config.withCredentials, config.responseType); + } + + return promise; + + + /** + * Callback registered to $httpBackend(): + * - caches the response if desired + * - resolves the raw $http promise + * - calls $apply + */ + function done(status, response, headersString, statusText) { + if (cache) { + if (isSuccess(status)) { + cache.put(url, [status, response, parseHeaders(headersString), statusText]); + } else { + // remove promise from the cache + cache.remove(url); + } + } + + resolvePromise(response, status, headersString, statusText); + if (!$rootScope.$$phase) $rootScope.$apply(); + } + + + /** + * Resolves the raw $http promise. + */ + function resolvePromise(response, status, headers, statusText) { + // normalize internal statuses to 0 + status = Math.max(status, 0); + + (isSuccess(status) ? deferred.resolve : deferred.reject)({ + data: response, + status: status, + headers: headersGetter(headers), + config: config, + statusText : statusText + }); + } + + + function removePendingReq() { + var idx = indexOf($http.pendingRequests, config); + if (idx !== -1) $http.pendingRequests.splice(idx, 1); + } + } + + + function buildUrl(url, params) { + if (!params) return url; + var parts = []; + forEachSorted(params, function(value, key) { + if (value === null || isUndefined(value)) return; + if (!isArray(value)) value = [value]; + + forEach(value, function(v) { + if (isObject(v)) { + if (isDate(v)){ + v = v.toISOString(); + } else { + v = toJson(v); + } + } + parts.push(encodeUriQuery(key) + '=' + + encodeUriQuery(v)); + }); + }); + if(parts.length > 0) { + url += ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&'); + } + return url; + } + }]; +} + +function createXhr(method) { + //if IE and the method is not RFC2616 compliant, or if XMLHttpRequest + //is not available, try getting an ActiveXObject. Otherwise, use XMLHttpRequest + //if it is available + if (msie <= 8 && (!method.match(/^(get|post|head|put|delete|options)$/i) || + !window.XMLHttpRequest)) { + return new window.ActiveXObject("Microsoft.XMLHTTP"); + } else if (window.XMLHttpRequest) { + return new window.XMLHttpRequest(); + } + + throw minErr('$httpBackend')('noxhr', "This browser does not support XMLHttpRequest."); +} + +/** + * @ngdoc service + * @name $httpBackend + * @requires $window + * @requires $document + * + * @description + * HTTP backend used by the {@link ng.$http service} that delegates to + * XMLHttpRequest object or JSONP and deals with browser incompatibilities. + * + * You should never need to use this service directly, instead use the higher-level abstractions: + * {@link ng.$http $http} or {@link ngResource.$resource $resource}. + * + * During testing this implementation is swapped with {@link ngMock.$httpBackend mock + * $httpBackend} which can be trained with responses. + */ +function $HttpBackendProvider() { + this.$get = ['$browser', '$window', '$document', function($browser, $window, $document) { + return createHttpBackend($browser, createXhr, $browser.defer, $window.angular.callbacks, $document[0]); + }]; +} + +function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) { + var ABORTED = -1; + + // TODO(vojta): fix the signature + return function(method, url, post, callback, headers, timeout, withCredentials, responseType) { + var status; + $browser.$$incOutstandingRequestCount(); + url = url || $browser.url(); + + if (lowercase(method) == 'jsonp') { + var callbackId = '_' + (callbacks.counter++).toString(36); + callbacks[callbackId] = function(data) { + callbacks[callbackId].data = data; + callbacks[callbackId].called = true; + }; + + var jsonpDone = jsonpReq(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId), + callbackId, function(status, text) { + completeRequest(callback, status, callbacks[callbackId].data, "", text); + callbacks[callbackId] = noop; + }); + } else { + + var xhr = createXhr(method); + + xhr.open(method, url, true); + forEach(headers, function(value, key) { + if (isDefined(value)) { + xhr.setRequestHeader(key, value); + } + }); + + // In IE6 and 7, this might be called synchronously when xhr.send below is called and the + // response is in the cache. the promise api will ensure that to the app code the api is + // always async + xhr.onreadystatechange = function() { + // onreadystatechange might get called multiple times with readyState === 4 on mobile webkit caused by + // xhrs that are resolved while the app is in the background (see #5426). + // since calling completeRequest sets the `xhr` variable to null, we just check if it's not null before + // continuing + // + // we can't set xhr.onreadystatechange to undefined or delete it because that breaks IE8 (method=PATCH) and + // Safari respectively. + if (xhr && xhr.readyState == 4) { + var responseHeaders = null, + response = null, + statusText = ''; + + if(status !== ABORTED) { + responseHeaders = xhr.getAllResponseHeaders(); + + // responseText is the old-school way of retrieving response (supported by IE8 & 9) + // response/responseType properties were introduced in XHR Level2 spec (supported by IE10) + response = ('response' in xhr) ? xhr.response : xhr.responseText; + } + + // Accessing statusText on an aborted xhr object will + // throw an 'c00c023f error' in IE9 and lower, don't touch it. + if (!(status === ABORTED && msie < 10)) { + statusText = xhr.statusText; + } + + completeRequest(callback, + status || xhr.status, + response, + responseHeaders, + statusText); + } + }; + + if (withCredentials) { + xhr.withCredentials = true; + } + + if (responseType) { + try { + xhr.responseType = responseType; + } catch (e) { + // WebKit added support for the json responseType value on 09/03/2013 + // https://bugs.webkit.org/show_bug.cgi?id=73648. Versions of Safari prior to 7 are + // known to throw when setting the value "json" as the response type. Other older + // browsers implementing the responseType + // + // The json response type can be ignored if not supported, because JSON payloads are + // parsed on the client-side regardless. + if (responseType !== 'json') { + throw e; + } + } + } + + xhr.send(post || null); + } + + if (timeout > 0) { + var timeoutId = $browserDefer(timeoutRequest, timeout); + } else if (isPromiseLike(timeout)) { + timeout.then(timeoutRequest); + } + + + function timeoutRequest() { + status = ABORTED; + jsonpDone && jsonpDone(); + xhr && xhr.abort(); + } + + function completeRequest(callback, status, response, headersString, statusText) { + // cancel timeout and subsequent timeout promise resolution + timeoutId && $browserDefer.cancel(timeoutId); + jsonpDone = xhr = null; + + // fix status code when it is 0 (0 status is undocumented). + // Occurs when accessing file resources or on Android 4.1 stock browser + // while retrieving files from application cache. + if (status === 0) { + status = response ? 200 : urlResolve(url).protocol == 'file' ? 404 : 0; + } + + // normalize IE bug (http://bugs.jquery.com/ticket/1450) + status = status === 1223 ? 204 : status; + statusText = statusText || ''; + + callback(status, response, headersString, statusText); + $browser.$$completeOutstandingRequest(noop); + } + }; + + function jsonpReq(url, callbackId, done) { + // we can't use jQuery/jqLite here because jQuery does crazy shit with script elements, e.g.: + // - fetches local scripts via XHR and evals them + // - adds and immediately removes script elements from the document + var script = rawDocument.createElement('script'), callback = null; + script.type = "text/javascript"; + script.src = url; + script.async = true; + + callback = function(event) { + removeEventListenerFn(script, "load", callback); + removeEventListenerFn(script, "error", callback); + rawDocument.body.removeChild(script); + script = null; + var status = -1; + var text = "unknown"; + + if (event) { + if (event.type === "load" && !callbacks[callbackId].called) { + event = { type: "error" }; + } + text = event.type; + status = event.type === "error" ? 404 : 200; + } + + if (done) { + done(status, text); + } + }; + + addEventListenerFn(script, "load", callback); + addEventListenerFn(script, "error", callback); + + if (msie <= 8) { + script.onreadystatechange = function() { + if (isString(script.readyState) && /loaded|complete/.test(script.readyState)) { + script.onreadystatechange = null; + callback({ + type: 'load' + }); + } + }; + } + + rawDocument.body.appendChild(script); + return callback; + } +} + +var $interpolateMinErr = minErr('$interpolate'); + +/** + * @ngdoc provider + * @name $interpolateProvider + * @kind function + * + * @description + * + * Used for configuring the interpolation markup. Defaults to `{{` and `}}`. + * + * @example + + + +
+ //demo.label// +
+
+ + it('should interpolate binding with custom symbols', function() { + expect(element(by.binding('demo.label')).getText()).toBe('This binding is brought you by // interpolation symbols.'); + }); + +
+ */ +function $InterpolateProvider() { + var startSymbol = '{{'; + var endSymbol = '}}'; + + /** + * @ngdoc method + * @name $interpolateProvider#startSymbol + * @description + * Symbol to denote start of expression in the interpolated string. Defaults to `{{`. + * + * @param {string=} value new value to set the starting symbol to. + * @returns {string|self} Returns the symbol when used as getter and self if used as setter. + */ + this.startSymbol = function(value){ + if (value) { + startSymbol = value; + return this; + } else { + return startSymbol; + } + }; + + /** + * @ngdoc method + * @name $interpolateProvider#endSymbol + * @description + * Symbol to denote the end of expression in the interpolated string. Defaults to `}}`. + * + * @param {string=} value new value to set the ending symbol to. + * @returns {string|self} Returns the symbol when used as getter and self if used as setter. + */ + this.endSymbol = function(value){ + if (value) { + endSymbol = value; + return this; + } else { + return endSymbol; + } + }; + + + this.$get = ['$parse', '$exceptionHandler', '$sce', function($parse, $exceptionHandler, $sce) { + var startSymbolLength = startSymbol.length, + endSymbolLength = endSymbol.length; + + /** + * @ngdoc service + * @name $interpolate + * @kind function + * + * @requires $parse + * @requires $sce + * + * @description + * + * Compiles a string with markup into an interpolation function. This service is used by the + * HTML {@link ng.$compile $compile} service for data binding. See + * {@link ng.$interpolateProvider $interpolateProvider} for configuring the + * interpolation markup. + * + * + * ```js + * var $interpolate = ...; // injected + * var exp = $interpolate('Hello {{name | uppercase}}!'); + * expect(exp({name:'Angular'}).toEqual('Hello ANGULAR!'); + * ``` + * + * + * @param {string} text The text with markup to interpolate. + * @param {boolean=} mustHaveExpression if set to true then the interpolation string must have + * embedded expression in order to return an interpolation function. Strings with no + * embedded expression will return null for the interpolation function. + * @param {string=} trustedContext when provided, the returned function passes the interpolated + * result through {@link ng.$sce#getTrusted $sce.getTrusted(interpolatedResult, + * trustedContext)} before returning it. Refer to the {@link ng.$sce $sce} service that + * provides Strict Contextual Escaping for details. + * @returns {function(context)} an interpolation function which is used to compute the + * interpolated string. The function has these parameters: + * + * * `context`: an object against which any expressions embedded in the strings are evaluated + * against. + * + */ + function $interpolate(text, mustHaveExpression, trustedContext) { + var startIndex, + endIndex, + index = 0, + parts = [], + length = text.length, + hasInterpolation = false, + fn, + exp, + concat = []; + + while(index < length) { + if ( ((startIndex = text.indexOf(startSymbol, index)) != -1) && + ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) ) { + (index != startIndex) && parts.push(text.substring(index, startIndex)); + parts.push(fn = $parse(exp = text.substring(startIndex + startSymbolLength, endIndex))); + fn.exp = exp; + index = endIndex + endSymbolLength; + hasInterpolation = true; + } else { + // we did not find anything, so we have to add the remainder to the parts array + (index != length) && parts.push(text.substring(index)); + index = length; + } + } + + if (!(length = parts.length)) { + // we added, nothing, must have been an empty string. + parts.push(''); + length = 1; + } + + // Concatenating expressions makes it hard to reason about whether some combination of + // concatenated values are unsafe to use and could easily lead to XSS. By requiring that a + // single expression be used for iframe[src], object[src], etc., we ensure that the value + // that's used is assigned or constructed by some JS code somewhere that is more testable or + // make it obvious that you bound the value to some user controlled value. This helps reduce + // the load when auditing for XSS issues. + if (trustedContext && parts.length > 1) { + throw $interpolateMinErr('noconcat', + "Error while interpolating: {0}\nStrict Contextual Escaping disallows " + + "interpolations that concatenate multiple expressions when a trusted value is " + + "required. See http://docs.angularjs.org/api/ng.$sce", text); + } + + if (!mustHaveExpression || hasInterpolation) { + concat.length = length; + fn = function(context) { + try { + for(var i = 0, ii = length, part; i + * **Note**: Intervals created by this service must be explicitly destroyed when you are finished + * with them. In particular they are not automatically destroyed when a controller's scope or a + * directive's element are destroyed. + * You should take this into consideration and make sure to always cancel the interval at the + * appropriate moment. See the example below for more details on how and when to do this. + * + * + * @param {function()} fn A function that should be called repeatedly. + * @param {number} delay Number of milliseconds between each function call. + * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat + * indefinitely. + * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise + * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. + * @returns {promise} A promise which will be notified on each iteration. + * + * @example + * + * + * + * + *
+ *
+ * Date format:
+ * Current time is: + *
+ * Blood 1 : {{blood_1}} + * Blood 2 : {{blood_2}} + * + * + * + *
+ *
+ * + *
+ *
+ */ + function interval(fn, delay, count, invokeApply) { + var setInterval = $window.setInterval, + clearInterval = $window.clearInterval, + deferred = $q.defer(), + promise = deferred.promise, + iteration = 0, + skipApply = (isDefined(invokeApply) && !invokeApply); + + count = isDefined(count) ? count : 0; + + promise.then(null, null, fn); + + promise.$$intervalId = setInterval(function tick() { + deferred.notify(iteration++); + + if (count > 0 && iteration >= count) { + deferred.resolve(iteration); + clearInterval(promise.$$intervalId); + delete intervals[promise.$$intervalId]; + } + + if (!skipApply) $rootScope.$apply(); + + }, delay); + + intervals[promise.$$intervalId] = deferred; + + return promise; + } + + + /** + * @ngdoc method + * @name $interval#cancel + * + * @description + * Cancels a task associated with the `promise`. + * + * @param {promise} promise returned by the `$interval` function. + * @returns {boolean} Returns `true` if the task was successfully canceled. + */ + interval.cancel = function(promise) { + if (promise && promise.$$intervalId in intervals) { + intervals[promise.$$intervalId].reject('canceled'); + $window.clearInterval(promise.$$intervalId); + delete intervals[promise.$$intervalId]; + return true; + } + return false; + }; + + return interval; + }]; +} + +/** + * @ngdoc service + * @name $locale + * + * @description + * $locale service provides localization rules for various Angular components. As of right now the + * only public api is: + * + * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`) + */ +function $LocaleProvider(){ + this.$get = function() { + return { + id: 'en-us', + + NUMBER_FORMATS: { + DECIMAL_SEP: '.', + GROUP_SEP: ',', + PATTERNS: [ + { // Decimal Pattern + minInt: 1, + minFrac: 0, + maxFrac: 3, + posPre: '', + posSuf: '', + negPre: '-', + negSuf: '', + gSize: 3, + lgSize: 3 + },{ //Currency Pattern + minInt: 1, + minFrac: 2, + maxFrac: 2, + posPre: '\u00A4', + posSuf: '', + negPre: '(\u00A4', + negSuf: ')', + gSize: 3, + lgSize: 3 + } + ], + CURRENCY_SYM: '$' + }, + + DATETIME_FORMATS: { + MONTH: + 'January,February,March,April,May,June,July,August,September,October,November,December' + .split(','), + SHORTMONTH: 'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(','), + DAY: 'Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday'.split(','), + SHORTDAY: 'Sun,Mon,Tue,Wed,Thu,Fri,Sat'.split(','), + AMPMS: ['AM','PM'], + medium: 'MMM d, y h:mm:ss a', + short: 'M/d/yy h:mm a', + fullDate: 'EEEE, MMMM d, y', + longDate: 'MMMM d, y', + mediumDate: 'MMM d, y', + shortDate: 'M/d/yy', + mediumTime: 'h:mm:ss a', + shortTime: 'h:mm a' + }, + + pluralCat: function(num) { + if (num === 1) { + return 'one'; + } + return 'other'; + } + }; + }; +} + +var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/, + DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; +var $locationMinErr = minErr('$location'); + + +/** + * Encode path using encodeUriSegment, ignoring forward slashes + * + * @param {string} path Path to encode + * @returns {string} + */ +function encodePath(path) { + var segments = path.split('/'), + i = segments.length; + + while (i--) { + segments[i] = encodeUriSegment(segments[i]); + } + + return segments.join('/'); +} + +function parseAbsoluteUrl(absoluteUrl, locationObj, appBase) { + var parsedUrl = urlResolve(absoluteUrl, appBase); + + locationObj.$$protocol = parsedUrl.protocol; + locationObj.$$host = parsedUrl.hostname; + locationObj.$$port = int(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null; +} + + +function parseAppUrl(relativeUrl, locationObj, appBase) { + var prefixed = (relativeUrl.charAt(0) !== '/'); + if (prefixed) { + relativeUrl = '/' + relativeUrl; + } + var match = urlResolve(relativeUrl, appBase); + locationObj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ? + match.pathname.substring(1) : match.pathname); + locationObj.$$search = parseKeyValue(match.search); + locationObj.$$hash = decodeURIComponent(match.hash); + + // make sure path starts with '/'; + if (locationObj.$$path && locationObj.$$path.charAt(0) != '/') { + locationObj.$$path = '/' + locationObj.$$path; + } +} + + +/** + * + * @param {string} begin + * @param {string} whole + * @returns {string} returns text from whole after begin or undefined if it does not begin with + * expected string. + */ +function beginsWith(begin, whole) { + if (whole.indexOf(begin) === 0) { + return whole.substr(begin.length); + } +} + + +function stripHash(url) { + var index = url.indexOf('#'); + return index == -1 ? url : url.substr(0, index); +} + + +function stripFile(url) { + return url.substr(0, stripHash(url).lastIndexOf('/') + 1); +} + +/* return the server only (scheme://host:port) */ +function serverBase(url) { + return url.substring(0, url.indexOf('/', url.indexOf('//') + 2)); +} + + +/** + * LocationHtml5Url represents an url + * This object is exposed as $location service when HTML5 mode is enabled and supported + * + * @constructor + * @param {string} appBase application base URL + * @param {string} basePrefix url path prefix + */ +function LocationHtml5Url(appBase, basePrefix) { + this.$$html5 = true; + basePrefix = basePrefix || ''; + var appBaseNoFile = stripFile(appBase); + parseAbsoluteUrl(appBase, this, appBase); + + + /** + * Parse given html5 (regular) url string into properties + * @param {string} newAbsoluteUrl HTML5 url + * @private + */ + this.$$parse = function(url) { + var pathUrl = beginsWith(appBaseNoFile, url); + if (!isString(pathUrl)) { + throw $locationMinErr('ipthprfx', 'Invalid url "{0}", missing path prefix "{1}".', url, + appBaseNoFile); + } + + parseAppUrl(pathUrl, this, appBase); + + if (!this.$$path) { + this.$$path = '/'; + } + + this.$$compose(); + }; + + /** + * Compose url and update `absUrl` property + * @private + */ + this.$$compose = function() { + var search = toKeyValue(this.$$search), + hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; + + this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; + this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/' + }; + + this.$$parseLinkUrl = function(url, relHref) { + var appUrl, prevAppUrl; + var rewrittenUrl; + + if ( (appUrl = beginsWith(appBase, url)) !== undefined ) { + prevAppUrl = appUrl; + if ( (appUrl = beginsWith(basePrefix, appUrl)) !== undefined ) { + rewrittenUrl = appBaseNoFile + (beginsWith('/', appUrl) || appUrl); + } else { + rewrittenUrl = appBase + prevAppUrl; + } + } else if ( (appUrl = beginsWith(appBaseNoFile, url)) !== undefined ) { + rewrittenUrl = appBaseNoFile + appUrl; + } else if (appBaseNoFile == url + '/') { + rewrittenUrl = appBaseNoFile; + } + if (rewrittenUrl) { + this.$$parse(rewrittenUrl); + } + return !!rewrittenUrl; + }; +} + + +/** + * LocationHashbangUrl represents url + * This object is exposed as $location service when developer doesn't opt into html5 mode. + * It also serves as the base class for html5 mode fallback on legacy browsers. + * + * @constructor + * @param {string} appBase application base URL + * @param {string} hashPrefix hashbang prefix + */ +function LocationHashbangUrl(appBase, hashPrefix) { + var appBaseNoFile = stripFile(appBase); + + parseAbsoluteUrl(appBase, this, appBase); + + + /** + * Parse given hashbang url into properties + * @param {string} url Hashbang url + * @private + */ + this.$$parse = function(url) { + var withoutBaseUrl = beginsWith(appBase, url) || beginsWith(appBaseNoFile, url); + var withoutHashUrl = withoutBaseUrl.charAt(0) == '#' + ? beginsWith(hashPrefix, withoutBaseUrl) + : (this.$$html5) + ? withoutBaseUrl + : ''; + + if (!isString(withoutHashUrl)) { + throw $locationMinErr('ihshprfx', 'Invalid url "{0}", missing hash prefix "{1}".', url, + hashPrefix); + } + parseAppUrl(withoutHashUrl, this, appBase); + + this.$$path = removeWindowsDriveName(this.$$path, withoutHashUrl, appBase); + + this.$$compose(); + + /* + * In Windows, on an anchor node on documents loaded from + * the filesystem, the browser will return a pathname + * prefixed with the drive name ('/C:/path') when a + * pathname without a drive is set: + * * a.setAttribute('href', '/foo') + * * a.pathname === '/C:/foo' //true + * + * Inside of Angular, we're always using pathnames that + * do not include drive names for routing. + */ + function removeWindowsDriveName (path, url, base) { + /* + Matches paths for file protocol on windows, + such as /C:/foo/bar, and captures only /foo/bar. + */ + var windowsFilePathExp = /^\/[A-Z]:(\/.*)/; + + var firstPathSegmentMatch; + + //Get the relative path from the input URL. + if (url.indexOf(base) === 0) { + url = url.replace(base, ''); + } + + // The input URL intentionally contains a first path segment that ends with a colon. + if (windowsFilePathExp.exec(url)) { + return path; + } + + firstPathSegmentMatch = windowsFilePathExp.exec(path); + return firstPathSegmentMatch ? firstPathSegmentMatch[1] : path; + } + }; + + /** + * Compose hashbang url and update `absUrl` property + * @private + */ + this.$$compose = function() { + var search = toKeyValue(this.$$search), + hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; + + this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; + this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : ''); + }; + + this.$$parseLinkUrl = function(url, relHref) { + if(stripHash(appBase) == stripHash(url)) { + this.$$parse(url); + return true; + } + return false; + }; +} + + +/** + * LocationHashbangUrl represents url + * This object is exposed as $location service when html5 history api is enabled but the browser + * does not support it. + * + * @constructor + * @param {string} appBase application base URL + * @param {string} hashPrefix hashbang prefix + */ +function LocationHashbangInHtml5Url(appBase, hashPrefix) { + this.$$html5 = true; + LocationHashbangUrl.apply(this, arguments); + + var appBaseNoFile = stripFile(appBase); + + this.$$parseLinkUrl = function(url, relHref) { + var rewrittenUrl; + var appUrl; + + if ( appBase == stripHash(url) ) { + rewrittenUrl = url; + } else if ( (appUrl = beginsWith(appBaseNoFile, url)) ) { + rewrittenUrl = appBase + hashPrefix + appUrl; + } else if ( appBaseNoFile === url + '/') { + rewrittenUrl = appBaseNoFile; + } + if (rewrittenUrl) { + this.$$parse(rewrittenUrl); + } + return !!rewrittenUrl; + }; + + this.$$compose = function() { + var search = toKeyValue(this.$$search), + hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; + + this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; + // include hashPrefix in $$absUrl when $$url is empty so IE8 & 9 do not reload page because of removal of '#' + this.$$absUrl = appBase + hashPrefix + this.$$url; + }; + +} + + +LocationHashbangInHtml5Url.prototype = + LocationHashbangUrl.prototype = + LocationHtml5Url.prototype = { + + /** + * Are we in html5 mode? + * @private + */ + $$html5: false, + + /** + * Has any change been replacing ? + * @private + */ + $$replace: false, + + /** + * @ngdoc method + * @name $location#absUrl + * + * @description + * This method is getter only. + * + * Return full url representation with all segments encoded according to rules specified in + * [RFC 3986](http://www.ietf.org/rfc/rfc3986.txt). + * + * @return {string} full url + */ + absUrl: locationGetter('$$absUrl'), + + /** + * @ngdoc method + * @name $location#url + * + * @description + * This method is getter / setter. + * + * Return url (e.g. `/path?a=b#hash`) when called without any parameter. + * + * Change path, search and hash, when called with parameter and return `$location`. + * + * @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`) + * @return {string} url + */ + url: function(url) { + if (isUndefined(url)) + return this.$$url; + var match = PATH_MATCH.exec(url); if (match[1]) this.path(decodeURIComponent(match[1])); if (match[2] || match[1]) this.search(match[3] || ''); - this.hash(match[5] || '', replace); + this.hash(match[5] || ''); return this; }, /** * @ngdoc method - * @name ng.$location#protocol - * @methodOf ng.$location + * @name $location#protocol * * @description * This method is getter only. @@ -5311,8 +9560,7 @@ LocationUrl.prototype = { /** * @ngdoc method - * @name ng.$location#host - * @methodOf ng.$location + * @name $location#host * * @description * This method is getter only. @@ -5325,8 +9573,7 @@ LocationUrl.prototype = { /** * @ngdoc method - * @name ng.$location#port - * @methodOf ng.$location + * @name $location#port * * @description * This method is getter only. @@ -5339,8 +9586,7 @@ LocationUrl.prototype = { /** * @ngdoc method - * @name ng.$location#path - * @methodOf ng.$location + * @name $location#path * * @description * This method is getter / setter. @@ -5352,17 +9598,17 @@ LocationUrl.prototype = { * Note: Path should always begin with forward slash (/), this method will add the forward slash * if it is missing. * - * @param {string=} path New path + * @param {(string|number)=} path New path * @return {string} path */ path: locationGetterSetter('$$path', function(path) { + path = path !== null ? path.toString() : ''; return path.charAt(0) == '/' ? path : '/' + path; }), /** * @ngdoc method - * @name ng.$location#search - * @methodOf ng.$location + * @name $location#search * * @description * This method is getter / setter. @@ -5371,24 +9617,67 @@ LocationUrl.prototype = { * * Change search part when called with parameter and return `$location`. * - * @param {string|object=} search New search params - string or hash object - * @param {string=} paramValue If `search` is a string, then `paramValue` will override only a - * single search parameter. If the value is `null`, the parameter will be deleted. * - * @return {string} search + * ```js + * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * var searchObject = $location.search(); + * // => {foo: 'bar', baz: 'xoxo'} + * + * + * // set foo to 'yipee' + * $location.search('foo', 'yipee'); + * // => $location + * ``` + * + * @param {string|Object.|Object.>} search New search params - string or + * hash object. + * + * When called with a single argument the method acts as a setter, setting the `search` component + * of `$location` to the specified value. + * + * If the argument is a hash object containing an array of values, these values will be encoded + * as duplicate search parameters in the url. + * + * @param {(string|Number|Array|boolean)=} paramValue If `search` is a string or number, then `paramValue` + * will override only a single search property. + * + * If `paramValue` is an array, it will override the property of the `search` component of + * `$location` specified via the first argument. + * + * If `paramValue` is `null`, the property specified via the first argument will be deleted. + * + * If `paramValue` is `true`, the property specified via the first argument will be added with no + * value nor trailing equal sign. + * + * @return {Object} If called with no arguments returns the parsed `search` object. If called with + * one or more arguments returns `$location` object itself. */ search: function(search, paramValue) { - if (isUndefined(search)) - return this.$$search; + switch (arguments.length) { + case 0: + return this.$$search; + case 1: + if (isString(search) || isNumber(search)) { + search = search.toString(); + this.$$search = parseKeyValue(search); + } else if (isObject(search)) { + // remove object undefined or null properties + forEach(search, function(value, key) { + if (value == null) delete search[key]; + }); - if (isDefined(paramValue)) { - if (paramValue === null) { - delete this.$$search[search]; - } else { - this.$$search[search] = paramValue; - } - } else { - this.$$search = isString(search) ? parseKeyValue(search) : search; + this.$$search = search; + } else { + throw $locationMinErr('isrcharg', + 'The first argument of the `$location#search()` call must be a string or an object.'); + } + break; + default: + if (isUndefined(paramValue) || paramValue === null) { + delete this.$$search[search]; + } else { + this.$$search[search] = paramValue; + } } this.$$compose(); @@ -5397,8 +9686,7 @@ LocationUrl.prototype = { /** * @ngdoc method - * @name ng.$location#hash - * @methodOf ng.$location + * @name $location#hash * * @description * This method is getter / setter. @@ -5407,15 +9695,16 @@ LocationUrl.prototype = { * * Change hash fragment when called with parameter and return `$location`. * - * @param {string=} hash New hash fragment + * @param {(string|number)=} hash New hash fragment * @return {string} hash */ - hash: locationGetterSetter('$$hash', identity), + hash: locationGetterSetter('$$hash', function(hash) { + return hash !== null ? hash.toString() : ''; + }), /** * @ngdoc method - * @name ng.$location#replace - * @methodOf ng.$location + * @name $location#replace * * @description * If called, all changes to $location during current `$digest` will be replacing current history @@ -5427,21 +9716,6 @@ LocationUrl.prototype = { } }; -LocationHashbangUrl.prototype = inherit(LocationUrl.prototype); - -function LocationHashbangInHtml5Url(url, hashPrefix, appBaseUrl, baseExtra) { - LocationHashbangUrl.apply(this, arguments); - - - this.$$rewriteAppUrl = function(absoluteLinkUrl) { - if (absoluteLinkUrl.indexOf(appBaseUrl) == 0) { - return appBaseUrl + baseExtra + '#' + hashPrefix + absoluteLinkUrl.substr(appBaseUrl.length); - } - } -} - -LocationHashbangInHtml5Url.prototype = inherit(LocationHashbangUrl.prototype); - function locationGetter(property) { return function() { return this[property]; @@ -5463,16 +9737,14 @@ function locationGetterSetter(property, preprocess) { /** - * @ngdoc object - * @name ng.$location + * @ngdoc service + * @name $location * - * @requires $browser - * @requires $sniffer * @requires $rootElement * * @description * The $location service parses the URL in the browser address bar (based on the - * {@link https://developer.mozilla.org/en/window.location window.location}) and makes the URL + * [window.location](https://developer.mozilla.org/en/window.location)) and makes the URL * available to your application. Changes to the URL in the address bar are reflected into * $location service and changes to $location are reflected into the browser address bar. * @@ -5487,13 +9759,12 @@ function locationGetterSetter(property, preprocess) { * - Clicks on a link. * - Represents the URL object as a set of methods (protocol, host, port, path, search, hash). * - * For more information see {@link guide/dev_guide.services.$location Developer Guide: Angular - * Services: Using $location} + * For more information see {@link guide/$location Developer Guide: Using $location} */ /** - * @ngdoc object - * @name ng.$locationProvider + * @ngdoc provider + * @name $locationProvider * @description * Use the `$locationProvider` to configure how the application deep linking paths are stored. */ @@ -5502,9 +9773,8 @@ function $LocationProvider(){ html5Mode = false; /** - * @ngdoc property - * @name ng.$locationProvider#hashPrefix - * @methodOf ng.$locationProvider + * @ngdoc method + * @name $locationProvider#hashPrefix * @description * @param {string=} prefix Prefix for hash part (containing path and search) * @returns {*} current value if used as getter or itself (chaining) if used as setter @@ -5519,11 +9789,10 @@ function $LocationProvider(){ }; /** - * @ngdoc property - * @name ng.$locationProvider#html5Mode - * @methodOf ng.$locationProvider + * @ngdoc method + * @name $locationProvider#html5Mode * @description - * @param {string=} mode Use HTML5 strategy if available. + * @param {boolean=} mode Use HTML5 strategy if available. * @returns {*} current value if used as getter or itself (chaining) if used as setter */ this.html5Mode = function(mode) { @@ -5535,42 +9804,54 @@ function $LocationProvider(){ } }; + /** + * @ngdoc event + * @name $location#$locationChangeStart + * @eventType broadcast on root scope + * @description + * Broadcasted before a URL will change. This change can be prevented by calling + * `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on} for more + * details about event object. Upon successful change + * {@link ng.$location#events_$locationChangeSuccess $locationChangeSuccess} is fired. + * + * @param {Object} angularEvent Synthetic event object. + * @param {string} newUrl New URL + * @param {string=} oldUrl URL that was before it was changed. + */ + + /** + * @ngdoc event + * @name $location#$locationChangeSuccess + * @eventType broadcast on root scope + * @description + * Broadcasted after a URL was changed. + * + * @param {Object} angularEvent Synthetic event object. + * @param {string} newUrl New URL + * @param {string=} oldUrl URL that was before it was changed. + */ + this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement', function( $rootScope, $browser, $sniffer, $rootElement) { var $location, - basePath, - pathPrefix, - initUrl = $browser.url(), - initUrlParts = matchUrl(initUrl), - appBaseUrl; + LocationMode, + baseHref = $browser.baseHref(), // if base[href] is undefined, it defaults to '' + initialUrl = $browser.url(), + appBase; if (html5Mode) { - basePath = $browser.baseHref() || '/'; - pathPrefix = pathPrefixFromBase(basePath); - appBaseUrl = - composeProtocolHostPort(initUrlParts.protocol, initUrlParts.host, initUrlParts.port) + - pathPrefix + '/'; - - if ($sniffer.history) { - $location = new LocationUrl( - convertToHtml5Url(initUrl, basePath, hashPrefix), - pathPrefix, appBaseUrl); - } else { - $location = new LocationHashbangInHtml5Url( - convertToHashbangUrl(initUrl, basePath, hashPrefix), - hashPrefix, appBaseUrl, basePath.substr(pathPrefix.length + 1)); - } + appBase = serverBase(initialUrl) + (baseHref || '/'); + LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url; } else { - appBaseUrl = - composeProtocolHostPort(initUrlParts.protocol, initUrlParts.host, initUrlParts.port) + - (initUrlParts.path || '') + - (initUrlParts.search ? ('?' + initUrlParts.search) : '') + - '#' + hashPrefix + '/'; - - $location = new LocationHashbangUrl(initUrl, hashPrefix, appBaseUrl); + appBase = stripHash(initialUrl); + LocationMode = LocationHashbangUrl; } + $location = new LocationMode(appBase, '#' + hashPrefix); + $location.$$parseLinkUrl(initialUrl, initialUrl); + + var IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i; - $rootElement.bind('click', function(event) { + $rootElement.on('click', function(event) { // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) // currently we open nice url link and redirect then @@ -5584,37 +9865,56 @@ function $LocationProvider(){ if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return; } - var absHref = elm.prop('href'), - rewrittenUrl = $location.$$rewriteAppUrl(absHref); + var absHref = elm.prop('href'); + // get the actual href attribute - see + // http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx + var relHref = elm.attr('href') || elm.attr('xlink:href'); - if (absHref && !elm.attr('target') && rewrittenUrl) { - // update location manually - $location.$$parse(rewrittenUrl); - $rootScope.$apply(); - event.preventDefault(); - // hack to work around FF6 bug 684208 when scenario runner clicks on links - window.angular['ff-684208-preventDefault'] = true; + if (isObject(absHref) && absHref.toString() === '[object SVGAnimatedString]') { + // SVGAnimatedString.animVal should be identical to SVGAnimatedString.baseVal, unless during + // an animation. + absHref = urlResolve(absHref.animVal).href; + } + + // Ignore when url is started with javascript: or mailto: + if (IGNORE_URI_REGEXP.test(absHref)) return; + + if (absHref && !elm.attr('target') && !event.isDefaultPrevented()) { + if ($location.$$parseLinkUrl(absHref, relHref)) { + // We do a preventDefault for all urls that are part of the angular application, + // in html5mode and also without, so that we are able to abort navigation without + // getting double entries in the location history. + event.preventDefault(); + // update location manually + if ($location.absUrl() != $browser.url()) { + $rootScope.$apply(); + // hack to work around FF6 bug 684208 when scenario runner clicks on links + window.angular['ff-684208-preventDefault'] = true; + } + } } }); // rewrite hashbang url <> html5 url - if ($location.absUrl() != initUrl) { + if ($location.absUrl() != initialUrl) { $browser.url($location.absUrl(), true); } // update $location when $browser url changes $browser.onUrlChange(function(newUrl) { if ($location.absUrl() != newUrl) { - if ($rootScope.$broadcast('$locationChangeStart', newUrl, $location.absUrl()).defaultPrevented) { - $browser.url($location.absUrl()); - return; - } $rootScope.$evalAsync(function() { var oldUrl = $location.absUrl(); $location.$$parse(newUrl); - afterLocationChange(oldUrl); + if ($rootScope.$broadcast('$locationChangeStart', newUrl, + oldUrl).defaultPrevented) { + $location.$$parse(oldUrl); + $browser.url(oldUrl); + } else { + afterLocationChange(oldUrl); + } }); if (!$rootScope.$$phase) $rootScope.$digest(); } @@ -5652,26 +9952,30 @@ function $LocationProvider(){ } /** - * @ngdoc object - * @name ng.$log + * @ngdoc service + * @name $log * @requires $window * * @description - * Simple service for logging. Default implementation writes the message + * Simple service for logging. Default implementation safely writes the message * into the browser's console (if present). * * The main purpose of this service is to simplify debugging and troubleshooting. * + * The default is to log `debug` messages. You can use + * {@link ng.$logProvider ng.$logProvider#debugEnabled} to change this. + * * @example - + - function LogCtrl($scope, $log) { - $scope.$log = $log; - $scope.message = 'Hello World!'; - } + angular.module('logExample', []) + .controller('LogController', ['$scope', '$log', function($scope, $log) { + $scope.$log = $log; + $scope.message = 'Hello World!'; + }]); -
+

Reload this page with open console, enter text and hit the log button...

Message: @@ -5684,13 +9988,37 @@ function $LocationProvider(){ */ +/** + * @ngdoc provider + * @name $logProvider + * @description + * Use the `$logProvider` to configure how the application logs messages + */ function $LogProvider(){ + var debug = true, + self = this; + + /** + * @ngdoc method + * @name $logProvider#debugEnabled + * @description + * @param {boolean=} flag enable or disable debug level messages + * @returns {*} current value if used as getter or itself (chaining) if used as setter + */ + this.debugEnabled = function(flag) { + if (isDefined(flag)) { + debug = flag; + return this; + } else { + return debug; + } + }; + this.$get = ['$window', function($window){ return { /** * @ngdoc method - * @name ng.$log#log - * @methodOf ng.$log + * @name $log#log * * @description * Write a log message @@ -5699,8 +10027,16 @@ function $LogProvider(){ /** * @ngdoc method - * @name ng.$log#warn - * @methodOf ng.$log + * @name $log#info + * + * @description + * Write an information message + */ + info: consoleLog('info'), + + /** + * @ngdoc method + * @name $log#warn * * @description * Write a warning message @@ -5709,23 +10045,29 @@ function $LogProvider(){ /** * @ngdoc method - * @name ng.$log#info - * @methodOf ng.$log + * @name $log#error * * @description - * Write an information message + * Write an error message */ - info: consoleLog('info'), + error: consoleLog('error'), /** * @ngdoc method - * @name ng.$log#error - * @methodOf ng.$log + * @name $log#debug * * @description - * Write an error message + * Write a debug message */ - error: consoleLog('error') + debug: (function () { + var fn = consoleLog('debug'); + + return function() { + if (debug) { + fn.apply(self, arguments); + } + }; + }()) }; function formatError(arg) { @@ -5743,9 +10085,16 @@ function $LogProvider(){ function consoleLog(type) { var console = $window.console || {}, - logFn = console[type] || console.log || noop; + logFn = console[type] || console.log || noop, + hasApply = false; + + // Note: reading logFn.apply throws an error in IE11 in IE8 document mode. + // The reason behind this is that console.log has type "object" in IE8... + try { + hasApply = !!logFn.apply; + } catch (e) {} - if (logFn.apply) { + if (hasApply) { return function() { var args = []; forEach(arguments, function(arg) { @@ -5758,13 +10107,98 @@ function $LogProvider(){ // we are IE which either doesn't have window.console => this is noop and we do nothing, // or we are IE where console.log doesn't have apply so we log at least first 2 args return function(arg1, arg2) { - logFn(arg1, arg2); - } + logFn(arg1, arg2 == null ? '' : arg2); + }; } }]; } +var $parseMinErr = minErr('$parse'); +var promiseWarningCache = {}; +var promiseWarning; + +// Sandboxing Angular Expressions +// ------------------------------ +// Angular expressions are generally considered safe because these expressions only have direct +// access to `$scope` and locals. However, one can obtain the ability to execute arbitrary JS code by +// obtaining a reference to native JS functions such as the Function constructor. +// +// As an example, consider the following Angular expression: +// +// {}.toString.constructor('alert("evil JS code")') +// +// This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits +// against the expression language, but not to prevent exploits that were enabled by exposing +// sensitive JavaScript or browser APIs on Scope. Exposing such objects on a Scope is never a good +// practice and therefore we are not even trying to protect against interaction with an object +// explicitly exposed in this way. +// +// In general, it is not possible to access a Window object from an angular expression unless a +// window or some DOM object that has a reference to window is published onto a Scope. +// Similarly we prevent invocations of function known to be dangerous, as well as assignments to +// native objects. +// +// See https://docs.angularjs.org/guide/security + + +function ensureSafeMemberName(name, fullExpression) { + if (name === "__defineGetter__" || name === "__defineSetter__" + || name === "__lookupGetter__" || name === "__lookupSetter__" + || name === "__proto__") { + throw $parseMinErr('isecfld', + 'Attempting to access a disallowed field in Angular expressions! ' + +'Expression: {0}', fullExpression); + } + return name; +} + +function ensureSafeObject(obj, fullExpression) { + // nifty check if obj is Function that is fast and works across iframes and other contexts + if (obj) { + if (obj.constructor === obj) { + throw $parseMinErr('isecfn', + 'Referencing Function in Angular expressions is disallowed! Expression: {0}', + fullExpression); + } else if (// isWindow(obj) + obj.document && obj.location && obj.alert && obj.setInterval) { + throw $parseMinErr('isecwindow', + 'Referencing the Window in Angular expressions is disallowed! Expression: {0}', + fullExpression); + } else if (// isElement(obj) + obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) { + throw $parseMinErr('isecdom', + 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}', + fullExpression); + } else if (// block Object so that we can't get hold of dangerous Object.* methods + obj === Object) { + throw $parseMinErr('isecobj', + 'Referencing Object in Angular expressions is disallowed! Expression: {0}', + fullExpression); + } + } + return obj; +} + +var CALL = Function.prototype.call; +var APPLY = Function.prototype.apply; +var BIND = Function.prototype.bind; + +function ensureSafeFunction(obj, fullExpression) { + if (obj) { + if (obj.constructor === obj) { + throw $parseMinErr('isecfn', + 'Referencing Function in Angular expressions is disallowed! Expression: {0}', + fullExpression); + } else if (obj === CALL || obj === APPLY || (BIND && obj === BIND)) { + throw $parseMinErr('isecff', + 'Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}', + fullExpression); + } + } +} + var OPERATORS = { + /* jshint bitwise : false */ 'null':function(){return null;}, 'true':function(){return true;}, 'false':function(){return false;}, @@ -5778,12 +10212,17 @@ var OPERATORS = { return a; } return isDefined(b)?b:undefined;}, - '-':function(self, locals, a,b){a=a(self, locals); b=b(self, locals); return (isDefined(a)?a:0)-(isDefined(b)?b:0);}, + '-':function(self, locals, a,b){ + a=a(self, locals); b=b(self, locals); + return (isDefined(a)?a:0)-(isDefined(b)?b:0); + }, '*':function(self, locals, a,b){return a(self, locals)*b(self, locals);}, '/':function(self, locals, a,b){return a(self, locals)/b(self, locals);}, '%':function(self, locals, a,b){return a(self, locals)%b(self, locals);}, '^':function(self, locals, a,b){return a(self, locals)^b(self, locals);}, '=':noop, + '===':function(self, locals, a, b){return a(self, locals)===b(self, locals);}, + '!==':function(self, locals, a, b){return a(self, locals)!==b(self, locals);}, '==':function(self, locals, a,b){return a(self, locals)==b(self, locals);}, '!=':function(self, locals, a,b){return a(self, locals)!=b(self, locals);}, '<':function(self, locals, a,b){return a(self, locals) 0) { - var token = tokens[0]; + value.literal = !!value.literal; + value.constant = !!value.constant; + + return value; + }, + + primary: function () { + var primary; + if (this.expect('(')) { + primary = this.filterChain(); + this.consume(')'); + } else if (this.expect('[')) { + primary = this.arrayDeclaration(); + } else if (this.expect('{')) { + primary = this.object(); + } else { + var token = this.expect(); + primary = token.fn; + if (!primary) { + this.throwError('not a primary expression', token); + } + primary.literal = !!token.literal; + primary.constant = !!token.constant; + } + + var next, context; + while ((next = this.expect('(', '[', '.'))) { + if (next.text === '(') { + primary = this.functionCall(primary, context); + context = null; + } else if (next.text === '[') { + context = primary; + primary = this.objectIndex(primary); + } else if (next.text === '.') { + context = primary; + primary = this.fieldAccess(primary); + } else { + this.throwError('IMPOSSIBLE'); + } + } + return primary; + }, + + throwError: function(msg, token) { + throw $parseMinErr('syntax', + 'Syntax Error: Token \'{0}\' {1} at column {2} of the expression [{3}] starting at [{4}].', + token.text, msg, (token.index + 1), this.text, this.text.substring(token.index)); + }, + + peekToken: function() { + if (this.tokens.length === 0) + throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); + return this.tokens[0]; + }, + + peek: function(e1, e2, e3, e4) { + if (this.tokens.length > 0) { + var token = this.tokens[0]; var t = token.text; - if (t==e1 || t==e2 || t==e3 || t==e4 || + if (t === e1 || t === e2 || t === e3 || t === e4 || (!e1 && !e2 && !e3 && !e4)) { return token; } } return false; - } + }, - function expect(e1, e2, e3, e4){ - var token = peek(e1, e2, e3, e4); + expect: function(e1, e2, e3, e4){ + var token = this.peek(e1, e2, e3, e4); if (token) { - if (json && !token.json) { - throwError("is not valid json", token); - } - tokens.shift(); + this.tokens.shift(); return token; } return false; - } + }, - function consume(e1){ - if (!expect(e1)) { - throwError("is unexpected, expecting [" + e1 + "]", peek()); + consume: function(e1){ + if (!this.expect(e1)) { + this.throwError('is unexpected, expecting [' + e1 + ']', this.peek()); } - } + }, - function unaryFn(fn, right) { - return function(self, locals) { + unaryFn: function(fn, right) { + return extend(function(self, locals) { return fn(self, locals, right); - }; - } + }, { + constant:right.constant + }); + }, + + ternaryFn: function(left, middle, right){ + return extend(function(self, locals){ + return left(self, locals) ? middle(self, locals) : right(self, locals); + }, { + constant: left.constant && middle.constant && right.constant + }); + }, - function binaryFn(left, fn, right) { - return function(self, locals) { + binaryFn: function(left, fn, right) { + return extend(function(self, locals) { return fn(self, locals, left, right); - }; - } + }, { + constant:left.constant && right.constant + }); + }, - function statements() { + statements: function() { var statements = []; - while(true) { - if (tokens.length > 0 && !peek('}', ')', ';', ']')) - statements.push(filterChain()); - if (!expect(';')) { + while (true) { + if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']')) + statements.push(this.filterChain()); + if (!this.expect(';')) { // optimize for the common case where there is only one statement. // TODO(size): maybe we should not support multiple statements? - return statements.length == 1 - ? statements[0] - : function(self, locals){ - var value; - for ( var i = 0; i < statements.length; i++) { - var statement = statements[i]; - if (statement) - value = statement(self, locals); - } - return value; - }; + return (statements.length === 1) + ? statements[0] + : function(self, locals) { + var value; + for (var i = 0; i < statements.length; i++) { + var statement = statements[i]; + if (statement) { + value = statement(self, locals); + } + } + return value; + }; } } - } + }, - function _filterChain() { - var left = expression(); + filterChain: function() { + var left = this.expression(); var token; - while(true) { - if ((token = expect('|'))) { - left = binaryFn(left, token.fn, filter()); + while (true) { + if ((token = this.expect('|'))) { + left = this.binaryFn(left, token.fn, this.filter()); } else { return left; } } - } + }, - function filter() { - var token = expect(); - var fn = $filter(token.text); + filter: function() { + var token = this.expect(); + var fn = this.$filter(token.text); var argsFn = []; - while(true) { - if ((token = expect(':'))) { - argsFn.push(expression()); + while (true) { + if ((token = this.expect(':'))) { + argsFn.push(this.expression()); } else { - var fnInvoke = function(self, locals, input){ + var fnInvoke = function(self, locals, input) { var args = [input]; - for ( var i = 0; i < argsFn.length; i++) { + for (var i = 0; i < argsFn.length; i++) { args.push(argsFn[i](self, locals)); } return fn.apply(self, args); @@ -6172,3263 +10693,3663 @@ function parser(text, json, $filter, csp){ }; } } - } + }, - function expression() { - return assignment(); - } + expression: function() { + return this.assignment(); + }, - function _assignment() { - var left = logicalOR(); + assignment: function() { + var left = this.ternary(); var right; var token; - if ((token = expect('='))) { + if ((token = this.expect('='))) { if (!left.assign) { - throwError("implies assignment but [" + - text.substring(0, token.index) + "] can not be assigned to", token); + this.throwError('implies assignment but [' + + this.text.substring(0, token.index) + '] can not be assigned to', token); } - right = logicalOR(); - return function(scope, locals){ + right = this.ternary(); + return function(scope, locals) { return left.assign(scope, right(scope, locals), locals); }; + } + return left; + }, + + ternary: function() { + var left = this.logicalOR(); + var middle; + var token; + if ((token = this.expect('?'))) { + middle = this.assignment(); + if ((token = this.expect(':'))) { + return this.ternaryFn(left, middle, this.assignment()); + } else { + this.throwError('expected :', token); + } } else { return left; } - } + }, - function logicalOR() { - var left = logicalAND(); + logicalOR: function() { + var left = this.logicalAND(); var token; - while(true) { - if ((token = expect('||'))) { - left = binaryFn(left, token.fn, logicalAND()); + while (true) { + if ((token = this.expect('||'))) { + left = this.binaryFn(left, token.fn, this.logicalAND()); } else { return left; } } - } + }, - function logicalAND() { - var left = equality(); + logicalAND: function() { + var left = this.equality(); var token; - if ((token = expect('&&'))) { - left = binaryFn(left, token.fn, logicalAND()); + if ((token = this.expect('&&'))) { + left = this.binaryFn(left, token.fn, this.logicalAND()); } return left; - } + }, - function equality() { - var left = relational(); + equality: function() { + var left = this.relational(); var token; - if ((token = expect('==','!='))) { - left = binaryFn(left, token.fn, equality()); + if ((token = this.expect('==','!=','===','!=='))) { + left = this.binaryFn(left, token.fn, this.equality()); } return left; - } + }, - function relational() { - var left = additive(); + relational: function() { + var left = this.additive(); var token; - if ((token = expect('<', '>', '<=', '>='))) { - left = binaryFn(left, token.fn, relational()); + if ((token = this.expect('<', '>', '<=', '>='))) { + left = this.binaryFn(left, token.fn, this.relational()); } return left; - } + }, - function additive() { - var left = multiplicative(); + additive: function() { + var left = this.multiplicative(); var token; - while ((token = expect('+','-'))) { - left = binaryFn(left, token.fn, multiplicative()); + while ((token = this.expect('+','-'))) { + left = this.binaryFn(left, token.fn, this.multiplicative()); } return left; - } + }, - function multiplicative() { - var left = unary(); + multiplicative: function() { + var left = this.unary(); var token; - while ((token = expect('*','/','%'))) { - left = binaryFn(left, token.fn, unary()); + while ((token = this.expect('*','/','%'))) { + left = this.binaryFn(left, token.fn, this.unary()); } return left; - } + }, - function unary() { + unary: function() { var token; - if (expect('+')) { - return primary(); - } else if ((token = expect('-'))) { - return binaryFn(ZERO, token.fn, unary()); - } else if ((token = expect('!'))) { - return unaryFn(token.fn, unary()); + if (this.expect('+')) { + return this.primary(); + } else if ((token = this.expect('-'))) { + return this.binaryFn(Parser.ZERO, token.fn, this.unary()); + } else if ((token = this.expect('!'))) { + return this.unaryFn(token.fn, this.unary()); } else { - return primary(); - } - } - - - function primary() { - var primary; - if (expect('(')) { - primary = filterChain(); - consume(')'); - } else if (expect('[')) { - primary = arrayDeclaration(); - } else if (expect('{')) { - primary = object(); - } else { - var token = expect(); - primary = token.fn; - if (!primary) { - throwError("not a primary expression", token); - } + return this.primary(); } + }, - var next, context; - while ((next = expect('(', '[', '.'))) { - if (next.text === '(') { - primary = functionCall(primary, context); - context = null; - } else if (next.text === '[') { - context = primary; - primary = objectIndex(primary); - } else if (next.text === '.') { - context = primary; - primary = fieldAccess(primary); - } else { - throwError("IMPOSSIBLE"); + fieldAccess: function(object) { + var parser = this; + var field = this.expect().text; + var getter = getterFn(field, this.options, this.text); + + return extend(function(scope, locals, self) { + return getter(self || object(scope, locals)); + }, { + assign: function(scope, value, locals) { + var o = object(scope, locals); + if (!o) object.assign(scope, o = {}); + return setter(o, field, value, parser.text, parser.options); } - } - return primary; - } - - function _fieldAccess(object) { - var field = expect().text; - var getter = getterFn(field, csp); - return extend( - function(scope, locals, self) { - return getter(self || object(scope, locals), locals); - }, - { - assign:function(scope, value, locals) { - return setter(object(scope, locals), field, value); - } - } - ); - } + }); + }, - function _objectIndex(obj) { - var indexFn = expression(); - consume(']'); - return extend( - function(self, locals){ - var o = obj(self, locals), - i = indexFn(self, locals), - v, p; - - if (!o) return undefined; - v = o[i]; - if (v && v.then) { - p = v; - if (!('$$v' in v)) { - p.$$v = undefined; - p.then(function(val) { p.$$v = val; }); - } - v = v.$$v; - } - return v; - }, { - assign:function(self, value, locals){ - return obj(self, locals)[indexFn(self, locals)] = value; + objectIndex: function(obj) { + var parser = this; + + var indexFn = this.expression(); + this.consume(']'); + + return extend(function(self, locals) { + var o = obj(self, locals), + i = indexFn(self, locals), + v, p; + + ensureSafeMemberName(i, parser.text); + if (!o) return undefined; + v = ensureSafeObject(o[i], parser.text); + if (v && v.then && parser.options.unwrapPromises) { + p = v; + if (!('$$v' in v)) { + p.$$v = undefined; + p.then(function(val) { p.$$v = val; }); } - }); - } - - function _functionCall(fn, contextGetter) { - var argsFn = []; - if (peekToken().text != ')') { - do { - argsFn.push(expression()); - } while (expect(',')); - } - consume(')'); - return function(scope, locals){ - var args = [], - context = contextGetter ? contextGetter(scope, locals) : scope; - - for ( var i = 0; i < argsFn.length; i++) { - args.push(argsFn[i](scope, locals)); - } - var fnPtr = fn(scope, locals, context) || noop; - // IE stupidity! - return fnPtr.apply - ? fnPtr.apply(context, args) - : fnPtr(args[0], args[1], args[2], args[3], args[4]); - }; - } - - // This is used with json array declaration - function arrayDeclaration () { - var elementFns = []; - if (peekToken().text != ']') { - do { - elementFns.push(expression()); - } while (expect(',')); - } - consume(']'); - return function(self, locals){ - var array = []; - for ( var i = 0; i < elementFns.length; i++) { - array.push(elementFns[i](self, locals)); + v = v.$$v; } - return array; - }; - } - - function object () { - var keyValues = []; - if (peekToken().text != '}') { - do { - var token = expect(), - key = token.string || token.text; - consume(":"); - var value = expression(); - keyValues.push({key:key, value:value}); - } while (expect(',')); - } - consume('}'); - return function(self, locals){ - var object = {}; - for ( var i = 0; i < keyValues.length; i++) { - var keyValue = keyValues[i]; - object[keyValue.key] = keyValue.value(self, locals); + return v; + }, { + assign: function(self, value, locals) { + var key = ensureSafeMemberName(indexFn(self, locals), parser.text); + // prevent overwriting of Function.constructor which would break ensureSafeObject check + var o = ensureSafeObject(obj(self, locals), parser.text); + if (!o) obj.assign(self, o = {}); + return o[key] = value; } - return object; - }; - } -} - -////////////////////////////////////////////////// -// Parser helper functions -////////////////////////////////////////////////// - -function setter(obj, path, setValue) { - var element = path.split('.'); - for (var i = 0; element.length > 1; i++) { - var key = element.shift(); - var propertyObj = obj[key]; - if (!propertyObj) { - propertyObj = {}; - obj[key] = propertyObj; - } - obj = propertyObj; - } - obj[element.shift()] = setValue; - return setValue; -} - -/** - * Return the value accesible from the object by path. Any undefined traversals are ignored - * @param {Object} obj starting object - * @param {string} path path to traverse - * @param {boolean=true} bindFnToScope - * @returns value as accesbile by path - */ -//TODO(misko): this function needs to be removed -function getter(obj, path, bindFnToScope) { - if (!path) return obj; - var keys = path.split('.'); - var key; - var lastInstance = obj; - var len = keys.length; - - for (var i = 0; i < len; i++) { - key = keys[i]; - if (obj) { - obj = (lastInstance = obj)[key]; - } - } - if (!bindFnToScope && isFunction(obj)) { - return bind(lastInstance, obj); - } - return obj; -} - -var getterFnCache = {}; + }); + }, -/** - * Implementation of the "Black Hole" variant from: - * - http://jsperf.com/angularjs-parse-getter/4 - * - http://jsperf.com/path-evaluation-simplified/7 - */ -function cspSafeGetterFn(key0, key1, key2, key3, key4) { - return function(scope, locals) { - var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope, - promise; + functionCall: function(fn, contextGetter) { + var argsFn = []; + if (this.peekToken().text !== ')') { + do { + argsFn.push(this.expression()); + } while (this.expect(',')); + } + this.consume(')'); - if (pathVal === null || pathVal === undefined) return pathVal; + var parser = this; - pathVal = pathVal[key0]; - if (pathVal && pathVal.then) { - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - if (!key1 || pathVal === null || pathVal === undefined) return pathVal; + return function(scope, locals) { + var args = []; + var context = contextGetter ? contextGetter(scope, locals) : scope; - pathVal = pathVal[key1]; - if (pathVal && pathVal.then) { - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); + for (var i = 0; i < argsFn.length; i++) { + args.push(ensureSafeObject(argsFn[i](scope, locals), parser.text)); } - pathVal = pathVal.$$v; - } - if (!key2 || pathVal === null || pathVal === undefined) return pathVal; + var fnPtr = fn(scope, locals, context) || noop; - pathVal = pathVal[key2]; - if (pathVal && pathVal.then) { - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - if (!key3 || pathVal === null || pathVal === undefined) return pathVal; + ensureSafeObject(context, parser.text); + ensureSafeFunction(fnPtr, parser.text); - pathVal = pathVal[key3]; - if (pathVal && pathVal.then) { - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; + // IE stupidity! (IE doesn't have apply for some native functions) + var v = fnPtr.apply + ? fnPtr.apply(context, args) + : fnPtr(args[0], args[1], args[2], args[3], args[4]); + + return ensureSafeObject(v, parser.text); + }; + }, + + // This is used with json array declaration + arrayDeclaration: function () { + var elementFns = []; + var allConstant = true; + if (this.peekToken().text !== ']') { + do { + if (this.peek(']')) { + // Support trailing commas per ES5.1. + break; + } + var elementFn = this.expression(); + elementFns.push(elementFn); + if (!elementFn.constant) { + allConstant = false; + } + } while (this.expect(',')); } - if (!key4 || pathVal === null || pathVal === undefined) return pathVal; + this.consume(']'); - pathVal = pathVal[key4]; - if (pathVal && pathVal.then) { - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); + return extend(function(self, locals) { + var array = []; + for (var i = 0; i < elementFns.length; i++) { + array.push(elementFns[i](self, locals)); } - pathVal = pathVal.$$v; + return array; + }, { + literal: true, + constant: allConstant + }); + }, + + object: function () { + var keyValues = []; + var allConstant = true; + if (this.peekToken().text !== '}') { + do { + if (this.peek('}')) { + // Support trailing commas per ES5.1. + break; + } + var token = this.expect(), + key = token.string || token.text; + this.consume(':'); + var value = this.expression(); + keyValues.push({key: key, value: value}); + if (!value.constant) { + allConstant = false; + } + } while (this.expect(',')); } - return pathVal; - }; -} + this.consume('}'); -function getterFn(path, csp) { - if (getterFnCache.hasOwnProperty(path)) { - return getterFnCache[path]; + return extend(function(self, locals) { + var object = {}; + for (var i = 0; i < keyValues.length; i++) { + var keyValue = keyValues[i]; + object[keyValue.key] = keyValue.value(self, locals); + } + return object; + }, { + literal: true, + constant: allConstant + }); } +}; - var pathKeys = path.split('.'), - pathKeysLength = pathKeys.length, - fn; - if (csp) { - fn = (pathKeysLength < 6) - ? cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4]) - : function(scope, locals) { - var i = 0, val; - do { - val = cspSafeGetterFn( - pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++] - )(scope, locals); - - locals = undefined; // clear after first iteration - scope = val; - } while (i < pathKeysLength); - return val; - } - } else { - var code = 'var l, fn, p;\n'; - forEach(pathKeys, function(key, index) { - code += 'if(s === null || s === undefined) return s;\n' + - 'l=s;\n' + - 's='+ (index - // we simply dereference 's' on any .dot notation - ? 's' - // but if we are first then we check locals first, and if so read it first - : '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n' + - 'if (s && s.then) {\n' + - ' if (!("$$v" in s)) {\n' + - ' p=s;\n' + - ' p.$$v = undefined;\n' + - ' p.then(function(v) {p.$$v=v;});\n' + - '}\n' + - ' s=s.$$v\n' + - '}\n'; - }); - code += 'return s;'; - fn = Function('s', 'k', code); // s=scope, k=locals - fn.toString = function() { return code; }; - } +////////////////////////////////////////////////// +// Parser helper functions +////////////////////////////////////////////////// - return getterFnCache[path] = fn; -} +function setter(obj, path, setValue, fullExp, options) { + ensureSafeObject(obj, fullExp); -/////////////////////////////////// + //needed? + options = options || {}; -/** - * @ngdoc function - * @name ng.$parse - * @function - * - * @description - * - * Converts Angular {@link guide/expression expression} into a function. - * - *
- *   var getter = $parse('user.name');
- *   var setter = getter.assign;
- *   var context = {user:{name:'angular'}};
- *   var locals = {user:{name:'local'}};
- *
- *   expect(getter(context)).toEqual('angular');
- *   setter(context, 'newValue');
- *   expect(context.user.name).toEqual('newValue');
- *   expect(getter(context, locals)).toEqual('local');
- * 
- * - * - * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: - * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (tipically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. - * - * The return function also has an `assign` property, if the expression is assignable, which - * allows one to set values to expressions. - * - */ -function $ParseProvider() { - var cache = {}; - this.$get = ['$filter', '$sniffer', function($filter, $sniffer) { - return function(exp) { - switch(typeof exp) { - case 'string': - return cache.hasOwnProperty(exp) - ? cache[exp] - : cache[exp] = parser(exp, false, $filter, $sniffer.csp); - case 'function': - return exp; - default: - return noop; + var element = path.split('.'), key; + for (var i = 0; element.length > 1; i++) { + key = ensureSafeMemberName(element.shift(), fullExp); + var propertyObj = ensureSafeObject(obj[key], fullExp); + if (!propertyObj) { + propertyObj = {}; + obj[key] = propertyObj; + } + obj = propertyObj; + if (obj.then && options.unwrapPromises) { + promiseWarning(fullExp); + if (!("$$v" in obj)) { + (function(promise) { + promise.then(function(val) { promise.$$v = val; }); } + )(obj); } - }; - }]; + if (obj.$$v === undefined) { + obj.$$v = {}; + } + obj = obj.$$v; + } + } + key = ensureSafeMemberName(element.shift(), fullExp); + ensureSafeObject(obj[key], fullExp); + obj[key] = setValue; + return setValue; } -/** - * @ngdoc service - * @name ng.$q - * @requires $rootScope - * - * @description - * A promise/deferred implementation inspired by [Kris Kowal's Q](https://github.com/kriskowal/q). - * - * [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an - * interface for interacting with an object that represents the result of an action that is - * performed asynchronously, and may or may not be finished at any given point in time. - * - * From the perspective of dealing with error handling, deferred and promise APIs are to - * asynchronous programming what `try`, `catch` and `throw` keywords are to synchronous programming. - * - *
- *   // for the purpose of this example let's assume that variables `$q` and `scope` are
- *   // available in the current lexical scope (they could have been injected or passed in).
- *
- *   function asyncGreet(name) {
- *     var deferred = $q.defer();
- *
- *     setTimeout(function() {
- *       // since this fn executes async in a future turn of the event loop, we need to wrap
- *       // our code into an $apply call so that the model changes are properly observed.
- *       scope.$apply(function() {
- *         if (okToGreet(name)) {
- *           deferred.resolve('Hello, ' + name + '!');
- *         } else {
- *           deferred.reject('Greeting ' + name + ' is not allowed.');
- *         }
- *       });
- *     }, 1000);
- *
- *     return deferred.promise;
- *   }
- *
- *   var promise = asyncGreet('Robin Hood');
- *   promise.then(function(greeting) {
- *     alert('Success: ' + greeting);
- *   }, function(reason) {
- *     alert('Failed: ' + reason);
- *   });
- * 
- * - * At first it might not be obvious why this extra complexity is worth the trouble. The payoff - * comes in the way of - * [guarantees that promise and deferred APIs make](https://github.com/kriskowal/uncommonjs/blob/master/promises/specification.md). - * - * Additionally the promise api allows for composition that is very hard to do with the - * traditional callback ([CPS](http://en.wikipedia.org/wiki/Continuation-passing_style)) approach. - * For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the - * section on serial or parallel joining of promises. - * - * - * # The Deferred API - * - * A new instance of deferred is constructed by calling `$q.defer()`. - * - * The purpose of the deferred object is to expose the associated Promise instance as well as APIs - * that can be used for signaling the successful or unsuccessful completion of the task. - * - * **Methods** - * - * - `resolve(value)` – resolves the derived promise with the `value`. If the value is a rejection - * constructed via `$q.reject`, the promise will be rejected instead. - * - `reject(reason)` – rejects the derived promise with the `reason`. This is equivalent to - * resolving it with a rejection constructed via `$q.reject`. - * - * **Properties** - * - * - promise – `{Promise}` – promise object associated with this deferred. - * - * - * # The Promise API - * - * A new promise instance is created when a deferred instance is created and can be retrieved by - * calling `deferred.promise`. - * - * The purpose of the promise object is to allow for interested parties to get access to the result - * of the deferred task when it completes. - * - * **Methods** - * - * - `then(successCallback, errorCallback)` – regardless of when the promise was or will be resolved - * or rejected calls one of the success or error callbacks asynchronously as soon as the result - * is available. The callbacks are called with a single argument the result or rejection reason. - * - * This method *returns a new promise* which is resolved or rejected via the return value of the - * `successCallback` or `errorCallback`. - * - * - * # Chaining promises - * - * Because calling `then` api of a promise returns a new derived promise, it is easily possible - * to create a chain of promises: - * - *
- *   promiseB = promiseA.then(function(result) {
- *     return result + 1;
- *   });
- *
- *   // promiseB will be resolved immediately after promiseA is resolved and its value will be
- *   // the result of promiseA incremented by 1
- * 
- * - * It is possible to create chains of any length and since a promise can be resolved with another - * promise (which will defer its resolution further), it is possible to pause/defer resolution of - * the promises at any point in the chain. This makes it possible to implement powerful apis like - * $http's response interceptors. - * - * - * # Differences between Kris Kowal's Q and $q - * - * There are three main differences: - * - * - $q is integrated with the {@link ng.$rootScope.Scope} Scope model observation - * mechanism in angular, which means faster propagation of resolution or rejection into your - * models and avoiding unnecessary browser repaints, which would result in flickering UI. - * - $q promises are recognized by the templating engine in angular, which means that in templates - * you can treat promises attached to a scope as if they were the resulting values. - * - Q has many more features than $q, but that comes at a cost of bytes. $q is tiny, but contains - * all the important functionality needed for common async tasks. - * - * # Testing - * - *
- *    it('should simulate promise', inject(function($q, $rootScope) {
- *      var deferred = $q.defer();
- *      var promise = deferred.promise;
- *      var resolvedValue;
- * 
- *      promise.then(function(value) { resolvedValue = value; });
- *      expect(resolvedValue).toBeUndefined();
- * 
- *      // Simulate resolving of promise
- *      deferred.resolve(123);
- *      // Note that the 'then' function does not get called synchronously.
- *      // This is because we want the promise API to always be async, whether or not
- *      // it got called synchronously or asynchronously.
- *      expect(resolvedValue).toBeUndefined();
- * 
- *      // Propagate promise resolution to 'then' functions using $apply().
- *      $rootScope.$apply();
- *      expect(resolvedValue).toEqual(123);
- *    });
- *  
- */ -function $QProvider() { +var getterFnCacheDefault = {}; +var getterFnCacheExpensive = {}; - this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) { - return qFactory(function(callback) { - $rootScope.$evalAsync(callback); - }, $exceptionHandler); - }]; +function isPossiblyDangerousMemberName(name) { + return name == 'constructor'; } - /** - * Constructs a promise manager. - * - * @param {function(function)} nextTick Function for executing functions in the next turn. - * @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for - * debugging purposes. - * @returns {object} Promise manager. + * Implementation of the "Black Hole" variant from: + * - http://jsperf.com/angularjs-parse-getter/4 + * - http://jsperf.com/path-evaluation-simplified/7 */ -function qFactory(nextTick, exceptionHandler) { - - /** - * @ngdoc - * @name ng.$q#defer - * @methodOf ng.$q - * @description - * Creates a `Deferred` object which represents a task which will finish in the future. - * - * @returns {Deferred} Returns a new instance of deferred. - */ - var defer = function() { - var pending = [], - value, deferred; - - deferred = { - - resolve: function(val) { - if (pending) { - var callbacks = pending; - pending = undefined; - value = ref(val); +function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp, options) { + ensureSafeMemberName(key0, fullExp); + ensureSafeMemberName(key1, fullExp); + ensureSafeMemberName(key2, fullExp); + ensureSafeMemberName(key3, fullExp); + ensureSafeMemberName(key4, fullExp); + var eso = function(o) { + return ensureSafeObject(o, fullExp); + }; + var expensiveChecks = options.expensiveChecks; + var eso0 = (expensiveChecks || isPossiblyDangerousMemberName(key0)) ? eso : identity; + var eso1 = (expensiveChecks || isPossiblyDangerousMemberName(key1)) ? eso : identity; + var eso2 = (expensiveChecks || isPossiblyDangerousMemberName(key2)) ? eso : identity; + var eso3 = (expensiveChecks || isPossiblyDangerousMemberName(key3)) ? eso : identity; + var eso4 = (expensiveChecks || isPossiblyDangerousMemberName(key4)) ? eso : identity; - if (callbacks.length) { - nextTick(function() { - var callback; - for (var i = 0, ii = callbacks.length; i < ii; i++) { - callback = callbacks[i]; - value.then(callback[0], callback[1]); - } - }); - } - } - }, + return !options.unwrapPromises + ? function cspSafeGetter(scope, locals) { + var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope; + if (pathVal == null) return pathVal; + pathVal = eso0(pathVal[key0]); - reject: function(reason) { - deferred.resolve(reject(reason)); - }, + if (!key1) return pathVal; + if (pathVal == null) return undefined; + pathVal = eso1(pathVal[key1]); + if (!key2) return pathVal; + if (pathVal == null) return undefined; + pathVal = eso2(pathVal[key2]); - promise: { - then: function(callback, errback) { - var result = defer(); + if (!key3) return pathVal; + if (pathVal == null) return undefined; + pathVal = eso3(pathVal[key3]); - var wrappedCallback = function(value) { - try { - result.resolve((callback || defaultCallback)(value)); - } catch(e) { - exceptionHandler(e); - result.reject(e); + if (!key4) return pathVal; + if (pathVal == null) return undefined; + pathVal = eso4(pathVal[key4]); + + return pathVal; + } + : function cspSafePromiseEnabledGetter(scope, locals) { + var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope, + promise; + + if (pathVal == null) return pathVal; + + pathVal = eso0(pathVal[key0]); + if (pathVal && pathVal.then) { + promiseWarning(fullExp); + if (!("$$v" in pathVal)) { + promise = pathVal; + promise.$$v = undefined; + promise.then(function(val) { promise.$$v = eso0(val); }); } - }; + pathVal = eso0(pathVal.$$v); + } - var wrappedErrback = function(reason) { - try { - result.resolve((errback || defaultErrback)(reason)); - } catch(e) { - exceptionHandler(e); - result.reject(e); + if (!key1) return pathVal; + if (pathVal == null) return undefined; + pathVal = eso1(pathVal[key1]); + if (pathVal && pathVal.then) { + promiseWarning(fullExp); + if (!("$$v" in pathVal)) { + promise = pathVal; + promise.$$v = undefined; + promise.then(function(val) { promise.$$v = eso1(val); }); } - }; + pathVal = eso1(pathVal.$$v); + } - if (pending) { - pending.push([wrappedCallback, wrappedErrback]); - } else { - value.then(wrappedCallback, wrappedErrback); + if (!key2) return pathVal; + if (pathVal == null) return undefined; + pathVal = eso2(pathVal[key2]); + if (pathVal && pathVal.then) { + promiseWarning(fullExp); + if (!("$$v" in pathVal)) { + promise = pathVal; + promise.$$v = undefined; + promise.then(function(val) { promise.$$v = eso2(val); }); + } + pathVal = eso2(pathVal.$$v); } - return result.promise; - } - } - }; + if (!key3) return pathVal; + if (pathVal == null) return undefined; + pathVal = eso3(pathVal[key3]); + if (pathVal && pathVal.then) { + promiseWarning(fullExp); + if (!("$$v" in pathVal)) { + promise = pathVal; + promise.$$v = undefined; + promise.then(function(val) { promise.$$v = eso3(val); }); + } + pathVal = eso3(pathVal.$$v); + } - return deferred; + if (!key4) return pathVal; + if (pathVal == null) return undefined; + pathVal = eso4(pathVal[key4]); + if (pathVal && pathVal.then) { + promiseWarning(fullExp); + if (!("$$v" in pathVal)) { + promise = pathVal; + promise.$$v = undefined; + promise.then(function(val) { promise.$$v = eso4(val); }); + } + pathVal = eso4(pathVal.$$v); + } + return pathVal; + }; +} + +function getterFnWithExtraArgs(fn, fullExpression) { + return function(s, l) { + return fn(s, l, promiseWarning, ensureSafeObject, fullExpression); }; +} + +function getterFn(path, options, fullExp) { + var expensiveChecks = options.expensiveChecks; + var getterFnCache = (expensiveChecks ? getterFnCacheExpensive : getterFnCacheDefault); + // Check whether the cache has this getter already. + // We can use hasOwnProperty directly on the cache because we ensure, + // see below, that the cache never stores a path called 'hasOwnProperty' + if (getterFnCache.hasOwnProperty(path)) { + return getterFnCache[path]; + } + var pathKeys = path.split('.'), + pathKeysLength = pathKeys.length, + fn; - var ref = function(value) { - if (value && value.then) return value; - return { - then: function(callback) { - var result = defer(); - nextTick(function() { - result.resolve(callback(value)); - }); - return result.promise; + // http://jsperf.com/angularjs-parse-getter/6 + if (options.csp) { + if (pathKeysLength < 6) { + fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp, + options); + } else { + fn = function(scope, locals) { + var i = 0, val; + do { + val = cspSafeGetterFn(pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], + pathKeys[i++], fullExp, options)(scope, locals); + + locals = undefined; // clear after first iteration + scope = val; + } while (i < pathKeysLength); + return val; + }; + } + } else { + var code = 'var p;\n'; + if (expensiveChecks) { + code += 's = eso(s, fe);\nl = eso(l, fe);\n'; + } + var needsEnsureSafeObject = expensiveChecks; + forEach(pathKeys, function(key, index) { + ensureSafeMemberName(key, fullExp); + var lookupJs = (index + // we simply dereference 's' on any .dot notation + ? 's' + // but if we are first then we check locals first, and if so read it first + : '((l&&l.hasOwnProperty("' + key + '"))?l:s)') + '["' + key + '"]'; + var wrapWithEso = expensiveChecks || isPossiblyDangerousMemberName(key); + if (wrapWithEso) { + lookupJs = 'eso(' + lookupJs + ', fe)'; + needsEnsureSafeObject = true; } - }; + code += 'if(s == null) return undefined;\n' + + 's=' + lookupJs + ';\n'; + if (options.unwrapPromises) { + code += 'if (s && s.then) {\n' + + ' pw("' + fullExp.replace(/(["\r\n])/g, '\\$1') + '");\n' + + ' if (!("$$v" in s)) {\n' + + ' p=s;\n' + + ' p.$$v = undefined;\n' + + ' p.then(function(v) {p.$$v=' + (wrapWithEso ? 'eso(v)' : 'v') + ';});\n' + + '}\n' + + ' s=' + (wrapWithEso ? 'eso(s.$$v)' : 's.$$v') + '\n' + + '}\n'; + + } + }); + code += 'return s;'; + + /* jshint -W054 */ + // s=scope, l=locals, pw=promiseWarning, eso=ensureSafeObject, fe=fullExpression + var evaledFnGetter = new Function('s', 'l', 'pw', 'eso', 'fe', code); + /* jshint +W054 */ + evaledFnGetter.toString = valueFn(code); + if (needsEnsureSafeObject || options.unwrapPromises) { + evaledFnGetter = getterFnWithExtraArgs(evaledFnGetter, fullExp); + } + fn = evaledFnGetter; + } + + // Only cache the value if it's not going to mess up the cache object + // This is more performant that using Object.prototype.hasOwnProperty.call + if (path !== 'hasOwnProperty') { + getterFnCache[path] = fn; + } + return fn; +} + +/////////////////////////////////// + +/** + * @ngdoc service + * @name $parse + * @kind function + * + * @description + * + * Converts Angular {@link guide/expression expression} into a function. + * + * ```js + * var getter = $parse('user.name'); + * var setter = getter.assign; + * var context = {user:{name:'angular'}}; + * var locals = {user:{name:'local'}}; + * + * expect(getter(context)).toEqual('angular'); + * setter(context, 'newValue'); + * expect(context.user.name).toEqual('newValue'); + * expect(getter(context, locals)).toEqual('local'); + * ``` + * + * + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + * + * The returned function also has the following properties: + * * `literal` – `{boolean}` – whether the expression's top-level node is a JavaScript + * literal. + * * `constant` – `{boolean}` – whether the expression is made entirely of JavaScript + * constant literals. + * * `assign` – `{?function(context, value)}` – if the expression is assignable, this will be + * set to a function to change its value on the given context. + * + */ + + +/** + * @ngdoc provider + * @name $parseProvider + * @kind function + * + * @description + * `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse} + * service. + */ +function $ParseProvider() { + var cacheDefault = {}; + var cacheExpensive = {}; + + var $parseOptions = { + csp: false, + unwrapPromises: false, + logPromiseWarnings: true, + expensiveChecks: false }; /** - * @ngdoc - * @name ng.$q#reject - * @methodOf ng.$q + * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future. + * + * @ngdoc method + * @name $parseProvider#unwrapPromises * @description - * Creates a promise that is resolved as rejected with the specified `reason`. This api should be - * used to forward rejection in a chain of promises. If you are dealing with the last promise in - * a promise chain, you don't need to worry about it. * - * When comparing deferreds/promises to the familiar behavior of try/catch/throw, think of - * `reject` as the `throw` keyword in JavaScript. This also means that if you "catch" an error via - * a promise error callback and you want to forward the error to the promise derived from the - * current promise, you have to "rethrow" the error by returning a rejection constructed via - * `reject`. + * **This feature is deprecated, see deprecation notes below for more info** * - *
-   *   promiseB = promiseA.then(function(result) {
-   *     // success: do something and resolve promiseB
-   *     //          with the old or a new result
-   *     return result;
-   *   }, function(reason) {
-   *     // error: handle the error if possible and
-   *     //        resolve promiseB with newPromiseOrValue,
-   *     //        otherwise forward the rejection to promiseB
-   *     if (canHandle(reason)) {
-   *      // handle the error and recover
-   *      return newPromiseOrValue;
-   *     }
-   *     return $q.reject(reason);
-   *   });
-   * 
+ * If set to true (default is false), $parse will unwrap promises automatically when a promise is + * found at any part of the expression. In other words, if set to true, the expression will always + * result in a non-promise value. * - * @param {*} reason Constant, message, exception or an object representing the rejection reason. - * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. + * While the promise is unresolved, it's treated as undefined, but once resolved and fulfilled, + * the fulfillment value is used in place of the promise while evaluating the expression. + * + * **Deprecation notice** + * + * This is a feature that didn't prove to be wildly useful or popular, primarily because of the + * dichotomy between data access in templates (accessed as raw values) and controller code + * (accessed as promises). + * + * In most code we ended up resolving promises manually in controllers anyway and thus unifying + * the model access there. + * + * Other downsides of automatic promise unwrapping: + * + * - when building components it's often desirable to receive the raw promises + * - adds complexity and slows down expression evaluation + * - makes expression code pre-generation unattractive due to the amount of code that needs to be + * generated + * - makes IDE auto-completion and tool support hard + * + * **Warning Logs** + * + * If the unwrapping is enabled, Angular will log a warning about each expression that unwraps a + * promise (to reduce the noise, each expression is logged only once). To disable this logging use + * `$parseProvider.logPromiseWarnings(false)` api. + * + * + * @param {boolean=} value New value. + * @returns {boolean|self} Returns the current setting when used as getter and self if used as + * setter. */ - var reject = function(reason) { - return { - then: function(callback, errback) { - var result = defer(); - nextTick(function() { - result.resolve((errback || defaultErrback)(reason)); - }); - return result.promise; - } - }; + this.unwrapPromises = function(value) { + if (isDefined(value)) { + $parseOptions.unwrapPromises = !!value; + return this; + } else { + return $parseOptions.unwrapPromises; + } }; /** - * @ngdoc - * @name ng.$q#when - * @methodOf ng.$q + * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future. + * + * @ngdoc method + * @name $parseProvider#logPromiseWarnings * @description - * Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. - * This is useful when you are dealing with an object that might or might not be a promise, or if - * the promise comes from a source that can't be trusted. * - * @param {*} value Value or a promise - * @returns {Promise} Returns a promise of the passed value or promise + * Controls whether Angular should log a warning on any encounter of a promise in an expression. + * + * The default is set to `true`. + * + * This setting applies only if `$parseProvider.unwrapPromises` setting is set to true as well. + * + * @param {boolean=} value New value. + * @returns {boolean|self} Returns the current setting when used as getter and self if used as + * setter. */ - var when = function(value, callback, errback) { - var result = defer(), - done; + this.logPromiseWarnings = function(value) { + if (isDefined(value)) { + $parseOptions.logPromiseWarnings = value; + return this; + } else { + return $parseOptions.logPromiseWarnings; + } + }; - var wrappedCallback = function(value) { - try { - return (callback || defaultCallback)(value); - } catch (e) { - exceptionHandler(e); - return reject(e); - } - }; - var wrappedErrback = function(reason) { - try { - return (errback || defaultErrback)(reason); - } catch (e) { - exceptionHandler(e); - return reject(e); - } + this.$get = ['$filter', '$sniffer', '$log', function($filter, $sniffer, $log) { + $parseOptions.csp = $sniffer.csp; + var $parseOptionsExpensive = { + csp: $parseOptions.csp, + unwrapPromises: $parseOptions.unwrapPromises, + logPromiseWarnings: $parseOptions.logPromiseWarnings, + expensiveChecks: true }; - nextTick(function() { - ref(value).then(function(value) { - if (done) return; - done = true; - result.resolve(ref(value).then(wrappedCallback, wrappedErrback)); - }, function(reason) { - if (done) return; - done = true; - result.resolve(wrappedErrback(reason)); - }); - }); - - return result.promise; - }; + promiseWarning = function promiseWarningFn(fullExp) { + if (!$parseOptions.logPromiseWarnings || promiseWarningCache.hasOwnProperty(fullExp)) return; + promiseWarningCache[fullExp] = true; + $log.warn('[$parse] Promise found in the expression `' + fullExp + '`. ' + + 'Automatic unwrapping of promises in Angular expressions is deprecated.'); + }; + return function(exp, expensiveChecks) { + var parsedExpression; - function defaultCallback(value) { - return value; - } + switch (typeof exp) { + case 'string': + var cache = (expensiveChecks ? cacheExpensive : cacheDefault); + if (cache.hasOwnProperty(exp)) { + return cache[exp]; + } - function defaultErrback(reason) { - return reject(reason); - } + var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions; + var lexer = new Lexer(parseOptions); + var parser = new Parser(lexer, $filter, parseOptions); + parsedExpression = parser.parse(exp); + if (exp !== 'hasOwnProperty') { + // Only cache the value if it's not going to mess up the cache object + // This is more performant that using Object.prototype.hasOwnProperty.call + cache[exp] = parsedExpression; + } - /** - * @ngdoc - * @name ng.$q#all - * @methodOf ng.$q - * @description - * Combines multiple promises into a single promise that is resolved when all of the input - * promises are resolved. - * - * @param {Array.} promises An array of promises. - * @returns {Promise} Returns a single promise that will be resolved with an array of values, - * each value corresponding to the promise at the same index in the `promises` array. If any of - * the promises is resolved with a rejection, this resulting promise will be resolved with the - * same rejection. - */ - function all(promises) { - var deferred = defer(), - counter = promises.length, - results = []; - - if (counter) { - forEach(promises, function(promise, index) { - ref(promise).then(function(value) { - if (index in results) return; - results[index] = value; - if (!(--counter)) deferred.resolve(results); - }, function(reason) { - if (index in results) return; - deferred.reject(reason); - }); - }); - } else { - deferred.resolve(results); - } + return parsedExpression; - return deferred.promise; - } + case 'function': + return exp; - return { - defer: defer, - reject: reject, - when: when, - all: all - }; + default: + return noop; + } + }; + }]; } /** - * @ngdoc object - * @name ng.$routeProvider - * @function + * @ngdoc service + * @name $q + * @requires $rootScope + * + * @description + * A service that helps you run functions asynchronously, and use their return values (or exceptions) + * when they are done processing. + * + * This is an implementation of promises/deferred objects inspired by + * [Kris Kowal's Q](https://github.com/kriskowal/q). + * + * [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an + * interface for interacting with an object that represents the result of an action that is + * performed asynchronously, and may or may not be finished at any given point in time. * - * @description + * From the perspective of dealing with error handling, deferred and promise APIs are to + * asynchronous programming what `try`, `catch` and `throw` keywords are to synchronous programming. + * + * ```js + * // for the purpose of this example let's assume that variables `$q`, `scope` and `okToGreet` + * // are available in the current lexical scope (they could have been injected or passed in). + * + * function asyncGreet(name) { + * var deferred = $q.defer(); + * + * setTimeout(function() { + * deferred.notify('About to greet ' + name + '.'); + * + * if (okToGreet(name)) { + * deferred.resolve('Hello, ' + name + '!'); + * } else { + * deferred.reject('Greeting ' + name + ' is not allowed.'); + * } + * }, 1000); + * + * return deferred.promise; + * } + * + * var promise = asyncGreet('Robin Hood'); + * promise.then(function(greeting) { + * alert('Success: ' + greeting); + * }, function(reason) { + * alert('Failed: ' + reason); + * }, function(update) { + * alert('Got notification: ' + update); + * }); + * ``` + * + * At first it might not be obvious why this extra complexity is worth the trouble. The payoff + * comes in the way of guarantees that promise and deferred APIs make, see + * https://github.com/kriskowal/uncommonjs/blob/master/promises/specification.md. + * + * Additionally the promise api allows for composition that is very hard to do with the + * traditional callback ([CPS](http://en.wikipedia.org/wiki/Continuation-passing_style)) approach. + * For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the + * section on serial or parallel joining of promises. + * + * + * # The Deferred API + * + * A new instance of deferred is constructed by calling `$q.defer()`. + * + * The purpose of the deferred object is to expose the associated Promise instance as well as APIs + * that can be used for signaling the successful or unsuccessful completion, as well as the status + * of the task. + * + * **Methods** + * + * - `resolve(value)` – resolves the derived promise with the `value`. If the value is a rejection + * constructed via `$q.reject`, the promise will be rejected instead. + * - `reject(reason)` – rejects the derived promise with the `reason`. This is equivalent to + * resolving it with a rejection constructed via `$q.reject`. + * - `notify(value)` - provides updates on the status of the promise's execution. This may be called + * multiple times before the promise is either resolved or rejected. + * + * **Properties** + * + * - promise – `{Promise}` – promise object associated with this deferred. + * + * + * # The Promise API + * + * A new promise instance is created when a deferred instance is created and can be retrieved by + * calling `deferred.promise`. + * + * The purpose of the promise object is to allow for interested parties to get access to the result + * of the deferred task when it completes. + * + * **Methods** + * + * - `then(successCallback, errorCallback, notifyCallback)` – regardless of when the promise was or + * will be resolved or rejected, `then` calls one of the success or error callbacks asynchronously + * as soon as the result is available. The callbacks are called with a single argument: the result + * or rejection reason. Additionally, the notify callback may be called zero or more times to + * provide a progress indication, before the promise is resolved or rejected. + * + * This method *returns a new promise* which is resolved or rejected via the return value of the + * `successCallback`, `errorCallback`. It also notifies via the return value of the + * `notifyCallback` method. The promise can not be resolved or rejected from the notifyCallback + * method. * - * Used for configuring routes. See {@link ng.$route $route} for an example. + * - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)` + * + * Because `catch` is a reserved word in JavaScript and reserved keywords are not supported as + * property names by ES3, you'll need to invoke the method like `promise['catch'](callback)` or + * `promise.then(null, errorCallback)` to make your code IE8 and Android 2.x compatible. + * + * - `finally(callback)` – allows you to observe either the fulfillment or rejection of a promise, + * but to do so without modifying the final value. This is useful to release resources or do some + * clean-up that needs to be done whether the promise was rejected or resolved. See the [full + * specification](https://github.com/kriskowal/q/wiki/API-Reference#promisefinallycallback) for + * more information. + * + * Because `finally` is a reserved word in JavaScript and reserved keywords are not supported as + * property names by ES3, you'll need to invoke the method like `promise['finally'](callback)` to + * make your code IE8 and Android 2.x compatible. + * + * # Chaining promises + * + * Because calling the `then` method of a promise returns a new derived promise, it is easily + * possible to create a chain of promises: + * + * ```js + * promiseB = promiseA.then(function(result) { + * return result + 1; + * }); + * + * // promiseB will be resolved immediately after promiseA is resolved and its value + * // will be the result of promiseA incremented by 1 + * ``` + * + * It is possible to create chains of any length and since a promise can be resolved with another + * promise (which will defer its resolution further), it is possible to pause/defer resolution of + * the promises at any point in the chain. This makes it possible to implement powerful APIs like + * $http's response interceptors. + * + * + * # Differences between Kris Kowal's Q and $q + * + * There are two main differences: + * + * - $q is integrated with the {@link ng.$rootScope.Scope} Scope model observation + * mechanism in angular, which means faster propagation of resolution or rejection into your + * models and avoiding unnecessary browser repaints, which would result in flickering UI. + * - Q has many more features than $q, but that comes at a cost of bytes. $q is tiny, but contains + * all the important functionality needed for common async tasks. + * + * # Testing + * + * ```js + * it('should simulate promise', inject(function($q, $rootScope) { + * var deferred = $q.defer(); + * var promise = deferred.promise; + * var resolvedValue; + * + * promise.then(function(value) { resolvedValue = value; }); + * expect(resolvedValue).toBeUndefined(); + * + * // Simulate resolving of promise + * deferred.resolve(123); + * // Note that the 'then' function does not get called synchronously. + * // This is because we want the promise API to always be async, whether or not + * // it got called synchronously or asynchronously. + * expect(resolvedValue).toBeUndefined(); + * + * // Propagate promise resolution to 'then' functions using $apply(). + * $rootScope.$apply(); + * expect(resolvedValue).toEqual(123); + * })); + * ``` */ -function $RouteProvider(){ - var routes = {}; - - /** - * @ngdoc method - * @name ng.$routeProvider#when - * @methodOf ng.$routeProvider - * - * @param {string} path Route path (matched against `$location.path`). If `$location.path` - * contains redundant trailing slash or is missing one, the route will still match and the - * `$location.path` will be updated to add or drop the trailing slash to exactly match the - * route definition. - * - * `path` can contain named groups starting with a colon (`:name`). All characters up to the - * next slash are matched and stored in `$routeParams` under the given `name` when the route - * matches. - * - * @param {Object} route Mapping information to be assigned to `$route.current` on route - * match. - * - * Object properties: - * - * - `controller` – `{(string|function()=}` – Controller fn that should be associated with newly - * created scope or the name of a {@link angular.Module#controller registered controller} - * if passed as a string. - * - `template` – `{string=}` – html template as a string that should be used by - * {@link ng.directive:ngView ngView} or - * {@link ng.directive:ngInclude ngInclude} directives. - * this property takes precedence over `templateUrl`. - * - `templateUrl` – `{string=}` – path to an html template that should be used by - * {@link ng.directive:ngView ngView}. - * - `resolve` - `{Object.=}` - An optional map of dependencies which should - * be injected into the controller. If any of these dependencies are promises, they will be - * resolved and converted to a value before the controller is instantiated and the - * `$routeChangeSuccess` event is fired. The map object is: - * - * - `key` – `{string}`: a name of a dependency to be injected into the controller. - * - `factory` - `{string|function}`: If `string` then it is an alias for a service. - * Otherwise if function, then it is {@link api/AUTO.$injector#invoke injected} - * and the return value is treated as the dependency. If the result is a promise, it is resolved - * before its value is injected into the controller. - * - * - `redirectTo` – {(string|function())=} – value to update - * {@link ng.$location $location} path with and trigger route redirection. - * - * If `redirectTo` is a function, it will be called with the following parameters: - * - * - `{Object.}` - route parameters extracted from the current - * `$location.path()` by applying the current route templateUrl. - * - `{string}` - current `$location.path()` - * - `{Object}` - current `$location.search()` - * - * The custom `redirectTo` function is expected to return a string which will be used - * to update `$location.path()` and `$location.search()`. - * - * - `[reloadOnSearch=true]` - {boolean=} - reload route when only $location.search() - * changes. - * - * If the option is set to `false` and url in the browser changes, then - * `$routeUpdate` event is broadcasted on the root scope. - * - * @returns {Object} self - * - * @description - * Adds a new route definition to the `$route` service. - */ - this.when = function(path, route) { - routes[path] = extend({reloadOnSearch: true}, route); +function $QProvider() { - // create redirection for trailing slashes - if (path) { - var redirectPath = (path[path.length-1] == '/') - ? path.substr(0, path.length-1) - : path +'/'; + this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) { + return qFactory(function(callback) { + $rootScope.$evalAsync(callback); + }, $exceptionHandler); + }]; +} - routes[redirectPath] = {redirectTo: path}; - } - return this; - }; +/** + * Constructs a promise manager. + * + * @param {function(Function)} nextTick Function for executing functions in the next turn. + * @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for + * debugging purposes. + * @returns {object} Promise manager. + */ +function qFactory(nextTick, exceptionHandler) { /** * @ngdoc method - * @name ng.$routeProvider#otherwise - * @methodOf ng.$routeProvider + * @name $q#defer + * @kind function * * @description - * Sets route definition that will be used on route change when no other route definition - * is matched. + * Creates a `Deferred` object which represents a task which will finish in the future. * - * @param {Object} params Mapping information to be assigned to `$route.current`. - * @returns {Object} self + * @returns {Deferred} Returns a new instance of deferred. */ - this.otherwise = function(params) { - this.when(null, params); - return this; - }; - - - this.$get = ['$rootScope', '$location', '$routeParams', '$q', '$injector', '$http', '$templateCache', - function( $rootScope, $location, $routeParams, $q, $injector, $http, $templateCache) { - - /** - * @ngdoc object - * @name ng.$route - * @requires $location - * @requires $routeParams - * - * @property {Object} current Reference to the current route definition. - * The route definition contains: - * - * - `controller`: The controller constructor as define in route definition. - * - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for - * controller instantiation. The `locals` contain - * the resolved values of the `resolve` map. Additionally the `locals` also contain: - * - * - `$scope` - The current route scope. - * - `$template` - The current route template HTML. - * - * @property {Array.} routes Array of all configured routes. - * - * @description - * Is used for deep-linking URLs to controllers and views (HTML partials). - * It watches `$location.url()` and tries to map the path to an existing route definition. - * - * You can define routes through {@link ng.$routeProvider $routeProvider}'s API. - * - * The `$route` service is typically used in conjunction with {@link ng.directive:ngView ngView} - * directive and the {@link ng.$routeParams $routeParams} service. - * - * @example - This example shows how changing the URL hash causes the `$route` to match a route against the - URL, and the `ngView` pulls in the partial. - - Note that this example is using {@link ng.directive:script inlined templates} - to get it working on jsfiddle as well. - - - -
- Choose: - Moby | - Moby: Ch1 | - Gatsby | - Gatsby: Ch4 | - Scarlet Letter
- -
-
- -
$location.path() = {{$location.path()}}
-
$route.current.templateUrl = {{$route.current.templateUrl}}
-
$route.current.params = {{$route.current.params}}
-
$route.current.scope.name = {{$route.current.scope.name}}
-
$routeParams = {{$routeParams}}
-
-
- - - controller: {{name}}
- Book Id: {{params.bookId}}
-
+ var defer = function() { + var pending = [], + value, deferred; - - controller: {{name}}
- Book Id: {{params.bookId}}
- Chapter Id: {{params.chapterId}} -
+ deferred = { - - angular.module('ngView', [], function($routeProvider, $locationProvider) { - $routeProvider.when('/Book/:bookId', { - templateUrl: 'book.html', - controller: BookCntl, - resolve: { - // I will cause a 1 second delay - delay: function($q, $timeout) { - var delay = $q.defer(); - $timeout(delay.resolve, 1000); - return delay.promise; - } - } - }); - $routeProvider.when('/Book/:bookId/ch/:chapterId', { - templateUrl: 'chapter.html', - controller: ChapterCntl - }); + resolve: function(val) { + if (pending) { + var callbacks = pending; + pending = undefined; + value = ref(val); - // configure html5 to get links working on jsfiddle - $locationProvider.html5Mode(true); - }); + if (callbacks.length) { + nextTick(function() { + var callback; + for (var i = 0, ii = callbacks.length; i < ii; i++) { + callback = callbacks[i]; + value.then(callback[0], callback[1], callback[2]); + } + }); + } + } + }, - function MainCntl($scope, $route, $routeParams, $location) { - $scope.$route = $route; - $scope.$location = $location; - $scope.$routeParams = $routeParams; - } - function BookCntl($scope, $routeParams) { - $scope.name = "BookCntl"; - $scope.params = $routeParams; - } + reject: function(reason) { + deferred.resolve(createInternalRejectedPromise(reason)); + }, - function ChapterCntl($scope, $routeParams) { - $scope.name = "ChapterCntl"; - $scope.params = $routeParams; - } - - - it('should load and compile correct template', function() { - element('a:contains("Moby: Ch1")').click(); - var content = element('.doc-example-live [ng-view]').text(); - expect(content).toMatch(/controller\: ChapterCntl/); - expect(content).toMatch(/Book Id\: Moby/); - expect(content).toMatch(/Chapter Id\: 1/); - - element('a:contains("Scarlet")').click(); - sleep(2); // promises are not part of scenario waiting - content = element('.doc-example-live [ng-view]').text(); - expect(content).toMatch(/controller\: BookCntl/); - expect(content).toMatch(/Book Id\: Scarlet/); - }); - -
- */ + notify: function(progress) { + if (pending) { + var callbacks = pending; - /** - * @ngdoc event - * @name ng.$route#$routeChangeStart - * @eventOf ng.$route - * @eventType broadcast on root scope - * @description - * Broadcasted before a route change. At this point the route services starts - * resolving all of the dependencies needed for the route change to occurs. - * Typically this involves fetching the view template as well as any dependencies - * defined in `resolve` route property. Once all of the dependencies are resolved - * `$routeChangeSuccess` is fired. - * - * @param {Route} next Future route information. - * @param {Route} current Current route information. - */ + if (pending.length) { + nextTick(function() { + var callback; + for (var i = 0, ii = callbacks.length; i < ii; i++) { + callback = callbacks[i]; + callback[2](progress); + } + }); + } + } + }, - /** - * @ngdoc event - * @name ng.$route#$routeChangeSuccess - * @eventOf ng.$route - * @eventType broadcast on root scope - * @description - * Broadcasted after a route dependencies are resolved. - * {@link ng.directive:ngView ngView} listens for the directive - * to instantiate the controller and render the view. - * - * @param {Object} angularEvent Synthetic event object. - * @param {Route} current Current route information. - * @param {Route|Undefined} previous Previous route information, or undefined if current is first route entered. - */ - /** - * @ngdoc event - * @name ng.$route#$routeChangeError - * @eventOf ng.$route - * @eventType broadcast on root scope - * @description - * Broadcasted if any of the resolve promises are rejected. - * - * @param {Route} current Current route information. - * @param {Route} previous Previous route information. - * @param {Route} rejection Rejection of the promise. Usually the error of the failed promise. - */ + promise: { + then: function(callback, errback, progressback) { + var result = defer(); - /** - * @ngdoc event - * @name ng.$route#$routeUpdate - * @eventOf ng.$route - * @eventType broadcast on root scope - * @description - * - * The `reloadOnSearch` property has been set to false, and we are reusing the same - * instance of the Controller. - */ + var wrappedCallback = function(value) { + try { + result.resolve((isFunction(callback) ? callback : defaultCallback)(value)); + } catch(e) { + result.reject(e); + exceptionHandler(e); + } + }; - var forceReload = false, - $route = { - routes: routes, + var wrappedErrback = function(reason) { + try { + result.resolve((isFunction(errback) ? errback : defaultErrback)(reason)); + } catch(e) { + result.reject(e); + exceptionHandler(e); + } + }; - /** - * @ngdoc method - * @name ng.$route#reload - * @methodOf ng.$route - * - * @description - * Causes `$route` service to reload the current route even if - * {@link ng.$location $location} hasn't changed. - * - * As a result of that, {@link ng.directive:ngView ngView} - * creates new scope, reinstantiates the controller. - */ - reload: function() { - forceReload = true; - $rootScope.$evalAsync(updateRoute); + var wrappedProgressback = function(progress) { + try { + result.notify((isFunction(progressback) ? progressback : defaultCallback)(progress)); + } catch(e) { + exceptionHandler(e); + } + }; + + if (pending) { + pending.push([wrappedCallback, wrappedErrback, wrappedProgressback]); + } else { + value.then(wrappedCallback, wrappedErrback, wrappedProgressback); } - }; - $rootScope.$on('$locationChangeSuccess', updateRoute); + return result.promise; + }, - return $route; + "catch": function(callback) { + return this.then(null, callback); + }, - ///////////////////////////////////////////////////// + "finally": function(callback) { - /** - * @param on {string} current url - * @param when {string} route when template to match the url against - * @return {?Object} - */ - function switchRouteMatcher(on, when) { - // TODO(i): this code is convoluted and inefficient, we should construct the route matching - // regex only once and then reuse it - - // Escape regexp special characters. - when = '^' + when.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&") + '$'; - var regex = '', - params = [], - dst = {}; - - var re = /:(\w+)/g, - paramMatch, - lastMatchedIndex = 0; - - while ((paramMatch = re.exec(when)) !== null) { - // Find each :param in `when` and replace it with a capturing group. - // Append all other sections of when unchanged. - regex += when.slice(lastMatchedIndex, paramMatch.index); - regex += '([^\\/]*)'; - params.push(paramMatch[1]); - lastMatchedIndex = re.lastIndex; - } - // Append trailing path part. - regex += when.substr(lastMatchedIndex); - - var match = on.match(new RegExp(regex)); - if (match) { - forEach(params, function(name, index) { - dst[name] = match[index + 1]; - }); - } - return match ? dst : null; - } - - function updateRoute() { - var next = parseRoute(), - last = $route.current; - - if (next && last && next.$$route === last.$$route - && equals(next.pathParams, last.pathParams) && !next.reloadOnSearch && !forceReload) { - last.params = next.params; - copy(last.params, $routeParams); - $rootScope.$broadcast('$routeUpdate', last); - } else if (next || last) { - forceReload = false; - $rootScope.$broadcast('$routeChangeStart', next, last); - $route.current = next; - if (next) { - if (next.redirectTo) { - if (isString(next.redirectTo)) { - $location.path(interpolate(next.redirectTo, next.params)).search(next.params) - .replace(); + function makePromise(value, resolved) { + var result = defer(); + if (resolved) { + result.resolve(value); } else { - $location.url(next.redirectTo(next.pathParams, $location.path(), $location.search())) - .replace(); + result.reject(value); } + return result.promise; } - } - - $q.when(next). - then(function() { - if (next) { - var keys = [], - values = [], - template; - forEach(next.resolve || {}, function(value, key) { - keys.push(key); - values.push(isString(value) ? $injector.get(value) : $injector.invoke(value)); - }); - if (isDefined(template = next.template)) { - } else if (isDefined(template = next.templateUrl)) { - template = $http.get(template, {cache: $templateCache}). - then(function(response) { return response.data; }); - } - if (isDefined(template)) { - keys.push('$template'); - values.push(template); - } - return $q.all(values).then(function(values) { - var locals = {}; - forEach(values, function(value, index) { - locals[keys[index]] = value; - }); - return locals; - }); + function handleCallback(value, isResolved) { + var callbackOutput = null; + try { + callbackOutput = (callback ||defaultCallback)(); + } catch(e) { + return makePromise(e, false); } - }). - // after route change - then(function(locals) { - if (next == $route.current) { - if (next) { - next.locals = locals; - copy(next.params, $routeParams); - } - $rootScope.$broadcast('$routeChangeSuccess', next, last); + if (isPromiseLike(callbackOutput)) { + return callbackOutput.then(function() { + return makePromise(value, isResolved); + }, function(error) { + return makePromise(error, false); + }); + } else { + return makePromise(value, isResolved); } + } + + return this.then(function(value) { + return handleCallback(value, true); }, function(error) { - if (next == $route.current) { - $rootScope.$broadcast('$routeChangeError', next, last, error); - } + return handleCallback(error, false); }); - } - } - - - /** - * @returns the current active route, by matching it against the URL - */ - function parseRoute() { - // Match a route - var params, match; - forEach(routes, function(route, path) { - if (!match && (params = switchRouteMatcher($location.path(), path))) { - match = inherit(route, { - params: extend({}, $location.search(), params), - pathParams: params}); - match.$$route = route; - } - }); - // No route matched; fallback to "otherwise" route - return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}}); - } - - /** - * @returns interpolation of the redirect path with the parametrs - */ - function interpolate(string, params) { - var result = []; - forEach((string||'').split(':'), function(segment, i) { - if (i == 0) { - result.push(segment); - } else { - var segmentMatch = segment.match(/(\w+)(.*)/); - var key = segmentMatch[1]; - result.push(params[key]); - result.push(segmentMatch[2] || ''); - delete params[key]; } - }); - return result.join(''); - } - }]; -} - -/** - * @ngdoc object - * @name ng.$routeParams - * @requires $route - * - * @description - * Current set of route parameters. The route parameters are a combination of the - * {@link ng.$location $location} `search()`, and `path()`. The `path` parameters - * are extracted when the {@link ng.$route $route} path is matched. - * - * In case of parameter name collision, `path` params take precedence over `search` params. - * - * The service guarantees that the identity of the `$routeParams` object will remain unchanged - * (but its properties will likely change) even when a route change occurs. - * - * @example - *
- *  // Given:
- *  // URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby
- *  // Route: /Chapter/:chapterId/Section/:sectionId
- *  //
- *  // Then
- *  $routeParams ==> {chapterId:1, sectionId:2, search:'moby'}
- * 
- */ -function $RouteParamsProvider() { - this.$get = valueFn({}); -} - -/** - * DESIGN NOTES - * - * The design decisions behind the scope are heavily favored for speed and memory consumption. - * - * The typical use of scope is to watch the expressions, which most of the time return the same - * value as last time so we optimize the operation. - * - * Closures construction is expensive in terms of speed as well as memory: - * - No closures, instead use prototypical inheritance for API - * - Internal state needs to be stored on scope directly, which means that private state is - * exposed as $$____ properties - * - * Loop operations are optimized by using while(count--) { ... } - * - this means that in order to keep the same order of execution as addition we have to add - * items to the array at the beginning (shift) instead of at the end (push) - * - * Child scopes are created and removed often - * - Using an array would be slow since inserts in middle are expensive so we use linked list - * - * There are few watches then a lot of observers. This is why you don't want the observer to be - * implemented in the same way as watch. Watch requires return of initialization function which - * are expensive to construct. - */ - + } + }; -/** - * @ngdoc object - * @name ng.$rootScopeProvider - * @description - * - * Provider for the $rootScope service. - */ + return deferred; + }; -/** - * @ngdoc function - * @name ng.$rootScopeProvider#digestTtl - * @methodOf ng.$rootScopeProvider - * @description - * - * Sets the number of digest iterations the scope should attempt to execute before giving up and - * assuming that the model is unstable. - * - * The current default is 10 iterations. - * - * @param {number} limit The number of digest iterations. - */ + var ref = function(value) { + if (isPromiseLike(value)) return value; + return { + then: function(callback) { + var result = defer(); + nextTick(function() { + result.resolve(callback(value)); + }); + return result.promise; + } + }; + }; -/** - * @ngdoc object - * @name ng.$rootScope - * @description - * - * Every application has a single root {@link ng.$rootScope.Scope scope}. - * All other scopes are child scopes of the root scope. Scopes provide mechanism for watching the model and provide - * event processing life-cycle. See {@link guide/scope developer guide on scopes}. - */ -function $RootScopeProvider(){ - var TTL = 10; - this.digestTtl = function(value) { - if (arguments.length) { - TTL = value; - } - return TTL; + /** + * @ngdoc method + * @name $q#reject + * @kind function + * + * @description + * Creates a promise that is resolved as rejected with the specified `reason`. This api should be + * used to forward rejection in a chain of promises. If you are dealing with the last promise in + * a promise chain, you don't need to worry about it. + * + * When comparing deferreds/promises to the familiar behavior of try/catch/throw, think of + * `reject` as the `throw` keyword in JavaScript. This also means that if you "catch" an error via + * a promise error callback and you want to forward the error to the promise derived from the + * current promise, you have to "rethrow" the error by returning a rejection constructed via + * `reject`. + * + * ```js + * promiseB = promiseA.then(function(result) { + * // success: do something and resolve promiseB + * // with the old or a new result + * return result; + * }, function(reason) { + * // error: handle the error if possible and + * // resolve promiseB with newPromiseOrValue, + * // otherwise forward the rejection to promiseB + * if (canHandle(reason)) { + * // handle the error and recover + * return newPromiseOrValue; + * } + * return $q.reject(reason); + * }); + * ``` + * + * @param {*} reason Constant, message, exception or an object representing the rejection reason. + * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. + */ + var reject = function(reason) { + var result = defer(); + result.reject(reason); + return result.promise; }; - this.$get = ['$injector', '$exceptionHandler', '$parse', - function( $injector, $exceptionHandler, $parse) { - - /** - * @ngdoc function - * @name ng.$rootScope.Scope - * - * @description - * A root scope can be retrieved using the {@link ng.$rootScope $rootScope} key from the - * {@link AUTO.$injector $injector}. Child scopes are created using the - * {@link ng.$rootScope.Scope#$new $new()} method. (Most scopes are created automatically when - * compiled HTML template is executed.) - * - * Here is a simple scope snippet to show how you can interact with the scope. - *
-        angular.injector(['ng']).invoke(function($rootScope) {
-           var scope = $rootScope.$new();
-           scope.salutation = 'Hello';
-           scope.name = 'World';
+  var createInternalRejectedPromise = function(reason) {
+    return {
+      then: function(callback, errback) {
+        var result = defer();
+        nextTick(function() {
+          try {
+            result.resolve((isFunction(errback) ? errback : defaultErrback)(reason));
+          } catch(e) {
+            result.reject(e);
+            exceptionHandler(e);
+          }
+        });
+        return result.promise;
+      }
+    };
+  };
 
-           expect(scope.greeting).toEqual(undefined);
 
-           scope.$watch('name', function() {
-             scope.greeting = scope.salutation + ' ' + scope.name + '!';
-           }); // initialize the watch
+  /**
+   * @ngdoc method
+   * @name $q#when
+   * @kind function
+   *
+   * @description
+   * Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise.
+   * This is useful when you are dealing with an object that might or might not be a promise, or if
+   * the promise comes from a source that can't be trusted.
+   *
+   * @param {*} value Value or a promise
+   * @returns {Promise} Returns a promise of the passed value or promise
+   */
+  var when = function(value, callback, errback, progressback) {
+    var result = defer(),
+        done;
 
-           expect(scope.greeting).toEqual(undefined);
-           scope.name = 'Misko';
-           // still old value, since watches have not been called yet
-           expect(scope.greeting).toEqual(undefined);
+    var wrappedCallback = function(value) {
+      try {
+        return (isFunction(callback) ? callback : defaultCallback)(value);
+      } catch (e) {
+        exceptionHandler(e);
+        return reject(e);
+      }
+    };
 
-           scope.$digest(); // fire all  the watches
-           expect(scope.greeting).toEqual('Hello Misko!');
-        });
-     * 
- * - * # Inheritance - * A scope can inherit from a parent scope, as in this example: - *
-         var parent = $rootScope;
-         var child = parent.$new();
+    var wrappedErrback = function(reason) {
+      try {
+        return (isFunction(errback) ? errback : defaultErrback)(reason);
+      } catch (e) {
+        exceptionHandler(e);
+        return reject(e);
+      }
+    };
 
-         parent.salutation = "Hello";
-         child.name = "World";
-         expect(child.salutation).toEqual('Hello');
+    var wrappedProgressback = function(progress) {
+      try {
+        return (isFunction(progressback) ? progressback : defaultCallback)(progress);
+      } catch (e) {
+        exceptionHandler(e);
+      }
+    };
 
-         child.salutation = "Welcome";
-         expect(child.salutation).toEqual('Welcome');
-         expect(parent.salutation).toEqual('Hello');
-     * 
- * - * - * @param {Object.=} providers Map of service factory which need to be provided - * for the current scope. Defaults to {@link ng}. - * @param {Object.=} instanceCache Provides pre-instantiated services which should - * append/override services provided by `providers`. This is handy when unit-testing and having - * the need to override a default service. - * @returns {Object} Newly created scope. - * - */ - function Scope() { - this.$id = nextUid(); - this.$$phase = this.$parent = this.$$watchers = - this.$$nextSibling = this.$$prevSibling = - this.$$childHead = this.$$childTail = null; - this['this'] = this.$root = this; - this.$$destroyed = false; - this.$$asyncQueue = []; - this.$$listeners = {}; - this.$$isolateBindings = {}; - } + nextTick(function() { + ref(value).then(function(value) { + if (done) return; + done = true; + result.resolve(ref(value).then(wrappedCallback, wrappedErrback, wrappedProgressback)); + }, function(reason) { + if (done) return; + done = true; + result.resolve(wrappedErrback(reason)); + }, function(progress) { + if (done) return; + result.notify(wrappedProgressback(progress)); + }); + }); - /** - * @ngdoc property - * @name ng.$rootScope.Scope#$id - * @propertyOf ng.$rootScope.Scope - * @returns {number} Unique scope ID (monotonically increasing alphanumeric sequence) useful for - * debugging. - */ + return result.promise; + }; - Scope.prototype = { - /** - * @ngdoc function - * @name ng.$rootScope.Scope#$new - * @methodOf ng.$rootScope.Scope - * @function - * - * @description - * Creates a new child {@link ng.$rootScope.Scope scope}. - * - * The parent scope will propagate the {@link ng.$rootScope.Scope#$digest $digest()} and - * {@link ng.$rootScope.Scope#$digest $digest()} events. The scope can be removed from the scope - * hierarchy using {@link ng.$rootScope.Scope#$destroy $destroy()}. - * - * {@link ng.$rootScope.Scope#$destroy $destroy()} must be called on a scope when it is desired for - * the scope and its child scopes to be permanently detached from the parent and thus stop - * participating in model change detection and listener notification by invoking. - * - * @param {boolean} isolate if true then the scope does not prototypically inherit from the - * parent scope. The scope is isolated, as it can not see parent scope properties. - * When creating widgets it is useful for the widget to not accidentally read parent - * state. - * - * @returns {Object} The newly created child scope. - * - */ - $new: function(isolate) { - var Child, - child; + function defaultCallback(value) { + return value; + } - if (isFunction(isolate)) { - // TODO: remove at some point - throw Error('API-CHANGE: Use $controller to instantiate controllers.'); - } - if (isolate) { - child = new Scope(); - child.$root = this.$root; - } else { - Child = function() {}; // should be anonymous; This is so that when the minifier munges - // the name it does not become random set of chars. These will then show up as class - // name in the debugger. - Child.prototype = this; - child = new Child(); - child.$id = nextUid(); - } - child['this'] = child; - child.$$listeners = {}; - child.$parent = this; - child.$$asyncQueue = []; - child.$$watchers = child.$$nextSibling = child.$$childHead = child.$$childTail = null; - child.$$prevSibling = this.$$childTail; - if (this.$$childHead) { - this.$$childTail.$$nextSibling = child; - this.$$childTail = child; - } else { - this.$$childHead = this.$$childTail = child; - } - return child; - }, - /** - * @ngdoc function - * @name ng.$rootScope.Scope#$watch - * @methodOf ng.$rootScope.Scope - * @function - * - * @description - * Registers a `listener` callback to be executed whenever the `watchExpression` changes. - * - * - The `watchExpression` is called on every call to {@link ng.$rootScope.Scope#$digest $digest()} and - * should return the value which will be watched. (Since {@link ng.$rootScope.Scope#$digest $digest()} - * reruns when it detects changes the `watchExpression` can execute multiple times per - * {@link ng.$rootScope.Scope#$digest $digest()} and should be idempotent.) - * - The `listener` is called only when the value from the current `watchExpression` and the - * previous call to `watchExpression` are not equal (with the exception of the initial run, - * see below). The inequality is determined according to - * {@link angular.equals} function. To save the value of the object for later comparison, the - * {@link angular.copy} function is used. It also means that watching complex options will - * have adverse memory and performance implications. - * - The watch `listener` may change the model, which may trigger other `listener`s to fire. This - * is achieved by rerunning the watchers until no changes are detected. The rerun iteration - * limit is 10 to prevent an infinite loop deadlock. - * - * - * If you want to be notified whenever {@link ng.$rootScope.Scope#$digest $digest} is called, - * you can register a `watchExpression` function with no `listener`. (Since `watchExpression` - * can execute multiple times per {@link ng.$rootScope.Scope#$digest $digest} cycle when a change is - * detected, be prepared for multiple calls to your listener.) - * - * After a watcher is registered with the scope, the `listener` fn is called asynchronously - * (via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}) to initialize the - * watcher. In rare cases, this is undesirable because the listener is called when the result - * of `watchExpression` didn't change. To detect this scenario within the `listener` fn, you - * can compare the `newVal` and `oldVal`. If these two values are identical (`===`) then the - * listener was called due to initialization. - * - * - * # Example - *
-           // let's assume that scope was dependency injected as the $rootScope
-           var scope = $rootScope;
-           scope.name = 'misko';
-           scope.counter = 0;
+  function defaultErrback(reason) {
+    return reject(reason);
+  }
 
-           expect(scope.counter).toEqual(0);
-           scope.$watch('name', function(newValue, oldValue) { scope.counter = scope.counter + 1; });
-           expect(scope.counter).toEqual(0);
 
-           scope.$digest();
-           // no variable change
-           expect(scope.counter).toEqual(0);
+  /**
+   * @ngdoc method
+   * @name $q#all
+   * @kind function
+   *
+   * @description
+   * Combines multiple promises into a single promise that is resolved when all of the input
+   * promises are resolved.
+   *
+   * @param {Array.|Object.} promises An array or hash of promises.
+   * @returns {Promise} Returns a single promise that will be resolved with an array/hash of values,
+   *   each value corresponding to the promise at the same index/key in the `promises` array/hash.
+   *   If any of the promises is resolved with a rejection, this resulting promise will be rejected
+   *   with the same rejection value.
+   */
+  function all(promises) {
+    var deferred = defer(),
+        counter = 0,
+        results = isArray(promises) ? [] : {};
+
+    forEach(promises, function(promise, key) {
+      counter++;
+      ref(promise).then(function(value) {
+        if (results.hasOwnProperty(key)) return;
+        results[key] = value;
+        if (!(--counter)) deferred.resolve(results);
+      }, function(reason) {
+        if (results.hasOwnProperty(key)) return;
+        deferred.reject(reason);
+      });
+    });
 
-           scope.name = 'adam';
-           scope.$digest();
-           expect(scope.counter).toEqual(1);
-       * 
- * - * - * - * @param {(function()|string)} watchExpression Expression that is evaluated on each - * {@link ng.$rootScope.Scope#$digest $digest} cycle. A change in the return value triggers a - * call to the `listener`. - * - * - `string`: Evaluated as {@link guide/expression expression} - * - `function(scope)`: called with current `scope` as a parameter. - * @param {(function()|string)=} listener Callback called whenever the return value of - * the `watchExpression` changes. - * - * - `string`: Evaluated as {@link guide/expression expression} - * - `function(newValue, oldValue, scope)`: called with current and previous values as parameters. - * - * @param {boolean=} objectEquality Compare object for equality rather than for reference. - * @returns {function()} Returns a deregistration function for this listener. - */ - $watch: function(watchExp, listener, objectEquality) { - var scope = this, - get = compileToFn(watchExp, 'watch'), - array = scope.$$watchers, - watcher = { - fn: listener, - last: initWatchVal, - get: get, - exp: watchExp, - eq: !!objectEquality - }; + if (counter === 0) { + deferred.resolve(results); + } - // in the case user pass string, we need to compile it, do we really need this ? - if (!isFunction(listener)) { - var listenFn = compileToFn(listener || noop, 'listener'); - watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);}; - } + return deferred.promise; + } - if (!array) { - array = scope.$$watchers = []; - } - // we use unshift since we use a while loop in $digest for speed. - // the while loop reads in reverse order. - array.unshift(watcher); + return { + defer: defer, + reject: reject, + when: when, + all: all + }; +} - return function() { - arrayRemove(array, watcher); +function $$RAFProvider(){ //rAF + this.$get = ['$window', '$timeout', function($window, $timeout) { + var requestAnimationFrame = $window.requestAnimationFrame || + $window.webkitRequestAnimationFrame || + $window.mozRequestAnimationFrame; + + var cancelAnimationFrame = $window.cancelAnimationFrame || + $window.webkitCancelAnimationFrame || + $window.mozCancelAnimationFrame || + $window.webkitCancelRequestAnimationFrame; + + var rafSupported = !!requestAnimationFrame; + var raf = rafSupported + ? function(fn) { + var id = requestAnimationFrame(fn); + return function() { + cancelAnimationFrame(id); + }; + } + : function(fn) { + var timer = $timeout(fn, 16.66, false); // 1000 / 60 = 16.666 + return function() { + $timeout.cancel(timer); + }; }; - }, - /** - * @ngdoc function - * @name ng.$rootScope.Scope#$digest - * @methodOf ng.$rootScope.Scope - * @function - * - * @description - * Processes all of the {@link ng.$rootScope.Scope#$watch watchers} of the current scope and its children. - * Because a {@link ng.$rootScope.Scope#$watch watcher}'s listener can change the model, the - * `$digest()` keeps calling the {@link ng.$rootScope.Scope#$watch watchers} until no more listeners are - * firing. This means that it is possible to get into an infinite loop. This function will throw - * `'Maximum iteration limit exceeded.'` if the number of iterations exceeds 10. - * - * Usually you don't call `$digest()` directly in - * {@link ng.directive:ngController controllers} or in - * {@link ng.$compileProvider#directive directives}. - * Instead a call to {@link ng.$rootScope.Scope#$apply $apply()} (typically from within a - * {@link ng.$compileProvider#directive directives}) will force a `$digest()`. - * - * If you want to be notified whenever `$digest()` is called, - * you can register a `watchExpression` function with {@link ng.$rootScope.Scope#$watch $watch()} - * with no `listener`. - * - * You may have a need to call `$digest()` from within unit-tests, to simulate the scope - * life-cycle. - * - * # Example - *
-           var scope = ...;
-           scope.name = 'misko';
-           scope.counter = 0;
+    raf.supported = rafSupported;
 
-           expect(scope.counter).toEqual(0);
-           scope.$watch('name', function(newValue, oldValue) {
-             scope.counter = scope.counter + 1;
-           });
-           expect(scope.counter).toEqual(0);
+    return raf;
+  }];
+}
 
-           scope.$digest();
-           // no variable change
-           expect(scope.counter).toEqual(0);
+/**
+ * DESIGN NOTES
+ *
+ * The design decisions behind the scope are heavily favored for speed and memory consumption.
+ *
+ * The typical use of scope is to watch the expressions, which most of the time return the same
+ * value as last time so we optimize the operation.
+ *
+ * Closures construction is expensive in terms of speed as well as memory:
+ *   - No closures, instead use prototypical inheritance for API
+ *   - Internal state needs to be stored on scope directly, which means that private state is
+ *     exposed as $$____ properties
+ *
+ * Loop operations are optimized by using while(count--) { ... }
+ *   - this means that in order to keep the same order of execution as addition we have to add
+ *     items to the array at the beginning (unshift) instead of at the end (push)
+ *
+ * Child scopes are created and removed often
+ *   - Using an array would be slow since inserts in middle are expensive so we use linked list
+ *
+ * There are few watches then a lot of observers. This is why you don't want the observer to be
+ * implemented in the same way as watch. Watch requires return of initialization function which
+ * are expensive to construct.
+ */
 
-           scope.name = 'adam';
-           scope.$digest();
-           expect(scope.counter).toEqual(1);
-       * 
- * - */ - $digest: function() { - var watch, value, last, - watchers, - asyncQueue, - length, - dirty, ttl = TTL, - next, current, target = this, - watchLog = [], - logIdx, logMsg; - beginPhase('$digest'); +/** + * @ngdoc provider + * @name $rootScopeProvider + * @description + * + * Provider for the $rootScope service. + */ - do { - dirty = false; - current = target; - do { - asyncQueue = current.$$asyncQueue; - while(asyncQueue.length) { - try { - current.$eval(asyncQueue.shift()); - } catch (e) { - $exceptionHandler(e); - } - } - if ((watchers = current.$$watchers)) { - // process our watches - length = watchers.length; - while (length--) { - try { - watch = watchers[length]; - // Most common watches are on primitives, in which case we can short - // circuit it with === operator, only when === fails do we use .equals - if ((value = watch.get(current)) !== (last = watch.last) && - !(watch.eq - ? equals(value, last) - : (typeof value == 'number' && typeof last == 'number' - && isNaN(value) && isNaN(last)))) { - dirty = true; - watch.last = watch.eq ? copy(value) : value; - watch.fn(value, ((last === initWatchVal) ? value : last), current); - if (ttl < 5) { - logIdx = 4 - ttl; - if (!watchLog[logIdx]) watchLog[logIdx] = []; - logMsg = (isFunction(watch.exp)) - ? 'fn: ' + (watch.exp.name || watch.exp.toString()) - : watch.exp; - logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last); - watchLog[logIdx].push(logMsg); - } - } - } catch (e) { - $exceptionHandler(e); - } - } - } +/** + * @ngdoc method + * @name $rootScopeProvider#digestTtl + * @description + * + * Sets the number of `$digest` iterations the scope should attempt to execute before giving up and + * assuming that the model is unstable. + * + * The current default is 10 iterations. + * + * In complex applications it's possible that the dependencies between `$watch`s will result in + * several digest iterations. However if an application needs more than the default 10 digest + * iterations for its model to stabilize then you should investigate what is causing the model to + * continuously change during the digest. + * + * Increasing the TTL could have performance implications, so you should not change it without + * proper justification. + * + * @param {number} limit The number of digest iterations. + */ - // Insanity Warning: scope depth-first traversal - // yes, this code is a bit crazy, but it works and we have tests to prove it! - // this piece should be kept in sync with the traversal in $broadcast - if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) { - while(current !== target && !(next = current.$$nextSibling)) { - current = current.$parent; - } - } - } while ((current = next)); - if(dirty && !(ttl--)) { - clearPhase(); - throw Error(TTL + ' $digest() iterations reached. Aborting!\n' + - 'Watchers fired in the last 5 iterations: ' + toJson(watchLog)); - } - } while (dirty || asyncQueue.length); +/** + * @ngdoc service + * @name $rootScope + * @description + * + * Every application has a single root {@link ng.$rootScope.Scope scope}. + * All other scopes are descendant scopes of the root scope. Scopes provide separation + * between the model and the view, via a mechanism for watching the model for changes. + * They also provide an event emission/broadcast and subscription facility. See the + * {@link guide/scope developer guide on scopes}. + */ +function $RootScopeProvider(){ + var TTL = 10; + var $rootScopeMinErr = minErr('$rootScope'); + var lastDirtyWatch = null; - clearPhase(); - }, + this.digestTtl = function(value) { + if (arguments.length) { + TTL = value; + } + return TTL; + }; + this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser', + function( $injector, $exceptionHandler, $parse, $browser) { - /** - * @ngdoc event - * @name ng.$rootScope.Scope#$destroy - * @eventOf ng.$rootScope.Scope - * @eventType broadcast on scope being destroyed - * - * @description - * Broadcasted when a scope and its children are being destroyed. - */ + /** + * @ngdoc type + * @name $rootScope.Scope + * + * @description + * A root scope can be retrieved using the {@link ng.$rootScope $rootScope} key from the + * {@link auto.$injector $injector}. Child scopes are created using the + * {@link ng.$rootScope.Scope#$new $new()} method. (Most scopes are created automatically when + * compiled HTML template is executed.) + * + * Here is a simple scope snippet to show how you can interact with the scope. + * ```html + * + * ``` + * + * # Inheritance + * A scope can inherit from a parent scope, as in this example: + * ```js + var parent = $rootScope; + var child = parent.$new(); - /** - * @ngdoc function - * @name ng.$rootScope.Scope#$destroy - * @methodOf ng.$rootScope.Scope - * @function - * - * @description - * Removes the current scope (and all of its children) from the parent scope. Removal implies - * that calls to {@link ng.$rootScope.Scope#$digest $digest()} will no longer - * propagate to the current scope and its children. Removal also implies that the current - * scope is eligible for garbage collection. - * - * The `$destroy()` is usually used by directives such as - * {@link ng.directive:ngRepeat ngRepeat} for managing the - * unrolling of the loop. - * - * Just before a scope is destroyed a `$destroy` event is broadcasted on this scope. - * Application code can register a `$destroy` event handler that will give it chance to - * perform any necessary cleanup. - */ - $destroy: function() { - // we can't destroy the root scope or a scope that has been already destroyed - if ($rootScope == this || this.$$destroyed) return; - var parent = this.$parent; + parent.salutation = "Hello"; + child.name = "World"; + expect(child.salutation).toEqual('Hello'); - this.$broadcast('$destroy'); - this.$$destroyed = true; + child.salutation = "Welcome"; + expect(child.salutation).toEqual('Welcome'); + expect(parent.salutation).toEqual('Hello'); + * ``` + * + * + * @param {Object.=} providers Map of service factory which need to be + * provided for the current scope. Defaults to {@link ng}. + * @param {Object.=} instanceCache Provides pre-instantiated services which should + * append/override services provided by `providers`. This is handy + * when unit-testing and having the need to override a default + * service. + * @returns {Object} Newly created scope. + * + */ + function Scope() { + this.$id = nextUid(); + this.$$phase = this.$parent = this.$$watchers = + this.$$nextSibling = this.$$prevSibling = + this.$$childHead = this.$$childTail = null; + this['this'] = this.$root = this; + this.$$destroyed = false; + this.$$asyncQueue = []; + this.$$postDigestQueue = []; + this.$$listeners = {}; + this.$$listenerCount = {}; + this.$$isolateBindings = {}; + } - if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling; - if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling; - if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling; - if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling; + /** + * @ngdoc property + * @name $rootScope.Scope#$id + * + * @description + * Unique scope ID (monotonically increasing) useful for debugging. + */ - // This is bogus code that works around Chrome's GC leak - // see: https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 - this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead = - this.$$childTail = null; - }, + /** + * @ngdoc property + * @name $rootScope.Scope#$parent + * + * @description + * Reference to the parent scope. + */ /** - * @ngdoc function - * @name ng.$rootScope.Scope#$eval - * @methodOf ng.$rootScope.Scope - * @function + * @ngdoc property + * @name $rootScope.Scope#$root * * @description - * Executes the `expression` on the current scope returning the result. Any exceptions in the - * expression are propagated (uncaught). This is useful when evaluating Angular expressions. - * - * # Example - *
-           var scope = ng.$rootScope.Scope();
-           scope.a = 1;
-           scope.b = 2;
-
-           expect(scope.$eval('a+b')).toEqual(3);
-           expect(scope.$eval(function(scope){ return scope.a + scope.b; })).toEqual(3);
-       * 
- * - * @param {(string|function())=} expression An angular expression to be executed. - * - * - `string`: execute using the rules as defined in {@link guide/expression expression}. - * - `function(scope)`: execute the function with the current `scope` parameter. - * - * @returns {*} The result of evaluating the expression. + * Reference to the root scope. */ - $eval: function(expr, locals) { - return $parse(expr)(this, locals); - }, + Scope.prototype = { + constructor: Scope, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$evalAsync - * @methodOf ng.$rootScope.Scope - * @function + * @ngdoc method + * @name $rootScope.Scope#$new + * @kind function * * @description - * Executes the expression on the current scope at a later point in time. - * - * The `$evalAsync` makes no guarantees as to when the `expression` will be executed, only that: + * Creates a new child {@link ng.$rootScope.Scope scope}. * - * - it will execute in the current script execution context (before any DOM rendering). - * - at least one {@link ng.$rootScope.Scope#$digest $digest cycle} will be performed after - * `expression` execution. + * The parent scope will propagate the {@link ng.$rootScope.Scope#$digest $digest()} event. + * The scope can be removed from the scope hierarchy using {@link ng.$rootScope.Scope#$destroy $destroy()}. * - * Any exceptions from the execution of the expression are forwarded to the - * {@link ng.$exceptionHandler $exceptionHandler} service. + * {@link ng.$rootScope.Scope#$destroy $destroy()} must be called on a scope when it is + * desired for the scope and its child scopes to be permanently detached from the parent and + * thus stop participating in model change detection and listener notification by invoking. * - * @param {(string|function())=} expression An angular expression to be executed. + * @param {boolean} isolate If true, then the scope does not prototypically inherit from the + * parent scope. The scope is isolated, as it can not see parent scope properties. + * When creating widgets, it is useful for the widget to not accidentally read parent + * state. * - * - `string`: execute using the rules as defined in {@link guide/expression expression}. - * - `function(scope)`: execute the function with the current `scope` parameter. + * @returns {Object} The newly created child scope. * */ - $evalAsync: function(expr) { - this.$$asyncQueue.push(expr); + $new: function(isolate) { + var ChildScope, + child; + + if (isolate) { + child = new Scope(); + child.$root = this.$root; + // ensure that there is just one async queue per $rootScope and its children + child.$$asyncQueue = this.$$asyncQueue; + child.$$postDigestQueue = this.$$postDigestQueue; + } else { + // Only create a child scope class if somebody asks for one, + // but cache it to allow the VM to optimize lookups. + if (!this.$$childScopeClass) { + this.$$childScopeClass = function() { + this.$$watchers = this.$$nextSibling = + this.$$childHead = this.$$childTail = null; + this.$$listeners = {}; + this.$$listenerCount = {}; + this.$id = nextUid(); + this.$$childScopeClass = null; + }; + this.$$childScopeClass.prototype = this; + } + child = new this.$$childScopeClass(); + } + child['this'] = child; + child.$parent = this; + child.$$prevSibling = this.$$childTail; + if (this.$$childHead) { + this.$$childTail.$$nextSibling = child; + this.$$childTail = child; + } else { + this.$$childHead = this.$$childTail = child; + } + return child; }, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$apply - * @methodOf ng.$rootScope.Scope - * @function + * @ngdoc method + * @name $rootScope.Scope#$watch + * @kind function * * @description - * `$apply()` is used to execute an expression in angular from outside of the angular framework. - * (For example from browser DOM events, setTimeout, XHR or third party libraries). - * Because we are calling into the angular framework we need to perform proper scope life-cycle - * of {@link ng.$exceptionHandler exception handling}, - * {@link ng.$rootScope.Scope#$digest executing watches}. + * Registers a `listener` callback to be executed whenever the `watchExpression` changes. * - * ## Life cycle + * - The `watchExpression` is called on every call to {@link ng.$rootScope.Scope#$digest + * $digest()} and should return the value that will be watched. (Since + * {@link ng.$rootScope.Scope#$digest $digest()} reruns when it detects changes the + * `watchExpression` can execute multiple times per + * {@link ng.$rootScope.Scope#$digest $digest()} and should be idempotent.) + * - The `listener` is called only when the value from the current `watchExpression` and the + * previous call to `watchExpression` are not equal (with the exception of the initial run, + * see below). Inequality is determined according to reference inequality, + * [strict comparison](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Comparison_Operators) + * via the `!==` Javascript operator, unless `objectEquality == true` + * (see next point) + * - When `objectEquality == true`, inequality of the `watchExpression` is determined + * according to the {@link angular.equals} function. To save the value of the object for + * later comparison, the {@link angular.copy} function is used. This therefore means that + * watching complex objects will have adverse memory and performance implications. + * - The watch `listener` may change the model, which may trigger other `listener`s to fire. + * This is achieved by rerunning the watchers until no changes are detected. The rerun + * iteration limit is 10 to prevent an infinite loop deadlock. * - * # Pseudo-Code of `$apply()` - *
-           function $apply(expr) {
-             try {
-               return $eval(expr);
-             } catch (e) {
-               $exceptionHandler(e);
-             } finally {
-               $root.$digest();
-             }
-           }
-       * 
* + * If you want to be notified whenever {@link ng.$rootScope.Scope#$digest $digest} is called, + * you can register a `watchExpression` function with no `listener`. (Since `watchExpression` + * can execute multiple times per {@link ng.$rootScope.Scope#$digest $digest} cycle when a + * change is detected, be prepared for multiple calls to your listener.) * - * Scope's `$apply()` method transitions through the following stages: + * After a watcher is registered with the scope, the `listener` fn is called asynchronously + * (via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}) to initialize the + * watcher. In rare cases, this is undesirable because the listener is called when the result + * of `watchExpression` didn't change. To detect this scenario within the `listener` fn, you + * can compare the `newVal` and `oldVal`. If these two values are identical (`===`) then the + * listener was called due to initialization. * - * 1. The {@link guide/expression expression} is executed using the - * {@link ng.$rootScope.Scope#$eval $eval()} method. - * 2. Any exceptions from the execution of the expression are forwarded to the - * {@link ng.$exceptionHandler $exceptionHandler} service. - * 3. The {@link ng.$rootScope.Scope#$watch watch} listeners are fired immediately after the expression - * was executed using the {@link ng.$rootScope.Scope#$digest $digest()} method. + * The example below contains an illustration of using a function as your $watch listener * * - * @param {(string|function())=} exp An angular expression to be executed. + * # Example + * ```js + // let's assume that scope was dependency injected as the $rootScope + var scope = $rootScope; + scope.name = 'misko'; + scope.counter = 0; + + expect(scope.counter).toEqual(0); + scope.$watch('name', function(newValue, oldValue) { + scope.counter = scope.counter + 1; + }); + expect(scope.counter).toEqual(0); + + scope.$digest(); + // the listener is always called during the first $digest loop after it was registered + expect(scope.counter).toEqual(1); + + scope.$digest(); + // but now it will not be called unless the value changes + expect(scope.counter).toEqual(1); + + scope.name = 'adam'; + scope.$digest(); + expect(scope.counter).toEqual(2); + + + + // Using a listener function + var food; + scope.foodCounter = 0; + expect(scope.foodCounter).toEqual(0); + scope.$watch( + // This is the listener function + function() { return food; }, + // This is the change handler + function(newValue, oldValue) { + if ( newValue !== oldValue ) { + // Only increment the counter if the value changed + scope.foodCounter = scope.foodCounter + 1; + } + } + ); + // No digest has been run so the counter will be zero + expect(scope.foodCounter).toEqual(0); + + // Run the digest but since food has not changed count will still be zero + scope.$digest(); + expect(scope.foodCounter).toEqual(0); + + // Update food and run digest. Now the counter will increment + food = 'cheeseburger'; + scope.$digest(); + expect(scope.foodCounter).toEqual(1); + + * ``` * - * - `string`: execute using the rules as defined in {@link guide/expression expression}. - * - `function(scope)`: execute the function with current `scope` parameter. * - * @returns {*} The result of evaluating the expression. - */ - $apply: function(expr) { - try { - beginPhase('$apply'); - return this.$eval(expr); - } catch (e) { - $exceptionHandler(e); - } finally { - clearPhase(); - try { - $rootScope.$digest(); - } catch (e) { - $exceptionHandler(e); - throw e; - } - } - }, - - /** - * @ngdoc function - * @name ng.$rootScope.Scope#$on - * @methodOf ng.$rootScope.Scope - * @function * - * @description - * Listens on events of a given type. See {@link ng.$rootScope.Scope#$emit $emit} for discussion of - * event life cycle. + * @param {(function()|string)} watchExpression Expression that is evaluated on each + * {@link ng.$rootScope.Scope#$digest $digest} cycle. A change in the return value triggers + * a call to the `listener`. * - * The event listener function format is: `function(event, args...)`. The `event` object - * passed into the listener has the following attributes: + * - `string`: Evaluated as {@link guide/expression expression} + * - `function(scope)`: called with current `scope` as a parameter. + * @param {(function()|string)=} listener Callback called whenever the return value of + * the `watchExpression` changes. * - * - `targetScope` - `{Scope}`: the scope on which the event was `$emit`-ed or `$broadcast`-ed. - * - `currentScope` - `{Scope}`: the current scope which is handling the event. - * - `name` - `{string}`: Name of the event. - * - `stopPropagation` - `{function=}`: calling `stopPropagation` function will cancel further event - * propagation (available only for events that were `$emit`-ed). - * - `preventDefault` - `{function}`: calling `preventDefault` sets `defaultPrevented` flag to true. - * - `defaultPrevented` - `{boolean}`: true if `preventDefault` was called. + * - `string`: Evaluated as {@link guide/expression expression} + * - `function(newValue, oldValue, scope)`: called with current and previous values as + * parameters. * - * @param {string} name Event name to listen on. - * @param {function(event, args...)} listener Function to call when the event is emitted. + * @param {boolean=} objectEquality Compare for object equality using {@link angular.equals} instead of + * comparing for reference equality. * @returns {function()} Returns a deregistration function for this listener. */ - $on: function(name, listener) { - var namedListeners = this.$$listeners[name]; - if (!namedListeners) { - this.$$listeners[name] = namedListeners = []; + $watch: function(watchExp, listener, objectEquality) { + var scope = this, + get = compileToFn(watchExp, 'watch'), + array = scope.$$watchers, + watcher = { + fn: listener, + last: initWatchVal, + get: get, + exp: watchExp, + eq: !!objectEquality + }; + + lastDirtyWatch = null; + + // in the case user pass string, we need to compile it, do we really need this ? + if (!isFunction(listener)) { + var listenFn = compileToFn(listener || noop, 'listener'); + watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);}; } - namedListeners.push(listener); - return function() { - namedListeners[indexOf(namedListeners, listener)] = null; + if (typeof watchExp == 'string' && get.constant) { + var originalFn = watcher.fn; + watcher.fn = function(newVal, oldVal, scope) { + originalFn.call(this, newVal, oldVal, scope); + arrayRemove(array, watcher); + }; + } + + if (!array) { + array = scope.$$watchers = []; + } + // we use unshift since we use a while loop in $digest for speed. + // the while loop reads in reverse order. + array.unshift(watcher); + + return function deregisterWatch() { + arrayRemove(array, watcher); + lastDirtyWatch = null; }; }, /** - * @ngdoc function - * @name ng.$rootScope.Scope#$emit - * @methodOf ng.$rootScope.Scope - * @function + * @ngdoc method + * @name $rootScope.Scope#$watchCollection + * @kind function * * @description - * Dispatches an event `name` upwards through the scope hierarchy notifying the - * registered {@link ng.$rootScope.Scope#$on} listeners. + * Shallow watches the properties of an object and fires whenever any of the properties change + * (for arrays, this implies watching the array items; for object maps, this implies watching + * the properties). If a change is detected, the `listener` callback is fired. * - * The event life cycle starts at the scope on which `$emit` was called. All - * {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get notified. - * Afterwards, the event traverses upwards toward the root scope and calls all registered - * listeners along the way. The event will stop propagating if one of the listeners cancels it. + * - The `obj` collection is observed via standard $watch operation and is examined on every + * call to $digest() to see if any items have been added, removed, or moved. + * - The `listener` is called whenever anything within the `obj` has changed. Examples include + * adding, removing, and moving items belonging to an object or array. * - * Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed - * onto the {@link ng.$exceptionHandler $exceptionHandler} service. * - * @param {string} name Event name to emit. - * @param {...*} args Optional set of arguments which will be passed onto the event listeners. - * @return {Object} Event object, see {@link ng.$rootScope.Scope#$on} - */ - $emit: function(name, args) { - var empty = [], - namedListeners, - scope = this, - stopPropagation = false, - event = { - name: name, - targetScope: scope, - stopPropagation: function() {stopPropagation = true;}, - preventDefault: function() { - event.defaultPrevented = true; - }, - defaultPrevented: false - }, - listenerArgs = concat([event], arguments, 1), - i, length; + * # Example + * ```js + $scope.names = ['igor', 'matias', 'misko', 'james']; + $scope.dataCount = 4; - do { - namedListeners = scope.$$listeners[name] || empty; - event.currentScope = scope; - for (i=0, length=namedListeners.length; i 1); + var changeDetected = 0; + var objGetter = $parse(obj); + var internalArray = []; + var internalObject = {}; + var initRun = true; + var oldLength = 0; + + function $watchCollectionWatch() { + newValue = objGetter(self); + var newLength, key, bothNaN; + + if (!isObject(newValue)) { // if primitive + if (oldValue !== newValue) { + oldValue = newValue; + changeDetected++; + } + } else if (isArrayLike(newValue)) { + if (oldValue !== internalArray) { + // we are transitioning from something which was not an array into array. + oldValue = internalArray; + oldLength = oldValue.length = 0; + changeDetected++; } - try { - listeners[i].apply(null, listenerArgs); - } catch(e) { - $exceptionHandler(e); + newLength = newValue.length; + + if (oldLength !== newLength) { + // if lengths do not match we need to trigger change notification + changeDetected++; + oldValue.length = oldLength = newLength; + } + // copy the items to oldValue and look for changes. + for (var i = 0; i < newLength; i++) { + bothNaN = (oldValue[i] !== oldValue[i]) && + (newValue[i] !== newValue[i]); + if (!bothNaN && (oldValue[i] !== newValue[i])) { + changeDetected++; + oldValue[i] = newValue[i]; + } + } + } else { + if (oldValue !== internalObject) { + // we are transitioning from something which was not an object into object. + oldValue = internalObject = {}; + oldLength = 0; + changeDetected++; + } + // copy the items to oldValue and look for changes. + newLength = 0; + for (key in newValue) { + if (newValue.hasOwnProperty(key)) { + newLength++; + if (oldValue.hasOwnProperty(key)) { + bothNaN = (oldValue[key] !== oldValue[key]) && + (newValue[key] !== newValue[key]); + if (!bothNaN && (oldValue[key] !== newValue[key])) { + changeDetected++; + oldValue[key] = newValue[key]; + } + } else { + oldLength++; + oldValue[key] = newValue[key]; + changeDetected++; + } + } + } + if (oldLength > newLength) { + // we used to have more keys, need to find them and destroy them. + changeDetected++; + for(key in oldValue) { + if (oldValue.hasOwnProperty(key) && !newValue.hasOwnProperty(key)) { + oldLength--; + delete oldValue[key]; + } + } } } + return changeDetected; + } - // Insanity Warning: scope depth-first traversal - // yes, this code is a bit crazy, but it works and we have tests to prove it! - // this piece should be kept in sync with the traversal in $digest - if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) { - while(current !== target && !(next = current.$$nextSibling)) { - current = current.$parent; + function $watchCollectionAction() { + if (initRun) { + initRun = false; + listener(newValue, newValue, self); + } else { + listener(newValue, veryOldValue, self); + } + + // make a copy for the next time a collection is changed + if (trackVeryOldValue) { + if (!isObject(newValue)) { + //primitive + veryOldValue = newValue; + } else if (isArrayLike(newValue)) { + veryOldValue = new Array(newValue.length); + for (var i = 0; i < newValue.length; i++) { + veryOldValue[i] = newValue[i]; + } + } else { // if object + veryOldValue = {}; + for (var key in newValue) { + if (hasOwnProperty.call(newValue, key)) { + veryOldValue[key] = newValue[key]; + } + } } } - } while ((current = next)); + } - return event; - } - }; + return this.$watch($watchCollectionWatch, $watchCollectionAction); + }, - var $rootScope = new Scope(); + /** + * @ngdoc method + * @name $rootScope.Scope#$digest + * @kind function + * + * @description + * Processes all of the {@link ng.$rootScope.Scope#$watch watchers} of the current scope and + * its children. Because a {@link ng.$rootScope.Scope#$watch watcher}'s listener can change + * the model, the `$digest()` keeps calling the {@link ng.$rootScope.Scope#$watch watchers} + * until no more listeners are firing. This means that it is possible to get into an infinite + * loop. This function will throw `'Maximum iteration limit exceeded.'` if the number of + * iterations exceeds 10. + * + * Usually, you don't call `$digest()` directly in + * {@link ng.directive:ngController controllers} or in + * {@link ng.$compileProvider#directive directives}. + * Instead, you should call {@link ng.$rootScope.Scope#$apply $apply()} (typically from within + * a {@link ng.$compileProvider#directive directives}), which will force a `$digest()`. + * + * If you want to be notified whenever `$digest()` is called, + * you can register a `watchExpression` function with + * {@link ng.$rootScope.Scope#$watch $watch()} with no `listener`. + * + * In unit tests, you may need to call `$digest()` to simulate the scope life cycle. + * + * # Example + * ```js + var scope = ...; + scope.name = 'misko'; + scope.counter = 0; - return $rootScope; + expect(scope.counter).toEqual(0); + scope.$watch('name', function(newValue, oldValue) { + scope.counter = scope.counter + 1; + }); + expect(scope.counter).toEqual(0); + scope.$digest(); + // the listener is always called during the first $digest loop after it was registered + expect(scope.counter).toEqual(1); - function beginPhase(phase) { - if ($rootScope.$$phase) { - throw Error($rootScope.$$phase + ' already in progress'); - } + scope.$digest(); + // but now it will not be called unless the value changes + expect(scope.counter).toEqual(1); - $rootScope.$$phase = phase; - } + scope.name = 'adam'; + scope.$digest(); + expect(scope.counter).toEqual(2); + * ``` + * + */ + $digest: function() { + var watch, value, last, + watchers, + asyncQueue = this.$$asyncQueue, + postDigestQueue = this.$$postDigestQueue, + length, + dirty, ttl = TTL, + next, current, target = this, + watchLog = [], + logIdx, logMsg, asyncTask; - function clearPhase() { - $rootScope.$$phase = null; - } + beginPhase('$digest'); + // Check for changes to browser url that happened in sync before the call to $digest + $browser.$$checkUrlChange(); - function compileToFn(exp, name) { - var fn = $parse(exp); - assertArgFn(fn, name); - return fn; - } + lastDirtyWatch = null; - /** - * function used as an initial value for watchers. - * because it's unique we can easily tell it apart from other values - */ - function initWatchVal() {} - }]; -} + do { // "while dirty" loop + dirty = false; + current = target; -/** - * !!! This is an undocumented "private" service !!! - * - * @name ng.$sniffer - * @requires $window - * - * @property {boolean} history Does the browser support html5 history api ? - * @property {boolean} hashchange Does the browser support hashchange event ? - * - * @description - * This is very simple implementation of testing browser's features. - */ -function $SnifferProvider() { - this.$get = ['$window', function($window) { - var eventSupport = {}, - android = int((/android (\d+)/.exec(lowercase($window.navigator.userAgent)) || [])[1]); + while(asyncQueue.length) { + try { + asyncTask = asyncQueue.shift(); + asyncTask.scope.$eval(asyncTask.expression); + } catch (e) { + clearPhase(); + $exceptionHandler(e); + } + lastDirtyWatch = null; + } - return { - // Android has history.pushState, but it does not update location correctly - // so let's not use the history API at all. - // http://code.google.com/p/android/issues/detail?id=17471 - // https://github.com/angular/angular.js/issues/904 - history: !!($window.history && $window.history.pushState && !(android < 4)), - hashchange: 'onhashchange' in $window && - // IE8 compatible mode lies - (!$window.document.documentMode || $window.document.documentMode > 7), - hasEvent: function(event) { - // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have - // it. In particular the event is not fired when backspace or delete key are pressed or - // when cut operation is performed. - if (event == 'input' && msie == 9) return false; + traverseScopesLoop: + do { // "traverse the scopes" loop + if ((watchers = current.$$watchers)) { + // process our watches + length = watchers.length; + while (length--) { + try { + watch = watchers[length]; + // Most common watches are on primitives, in which case we can short + // circuit it with === operator, only when === fails do we use .equals + if (watch) { + if ((value = watch.get(current)) !== (last = watch.last) && + !(watch.eq + ? equals(value, last) + : (typeof value === 'number' && typeof last === 'number' + && isNaN(value) && isNaN(last)))) { + dirty = true; + lastDirtyWatch = watch; + watch.last = watch.eq ? copy(value, null) : value; + watch.fn(value, ((last === initWatchVal) ? value : last), current); + if (ttl < 5) { + logIdx = 4 - ttl; + if (!watchLog[logIdx]) watchLog[logIdx] = []; + logMsg = (isFunction(watch.exp)) + ? 'fn: ' + (watch.exp.name || watch.exp.toString()) + : watch.exp; + logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last); + watchLog[logIdx].push(logMsg); + } + } else if (watch === lastDirtyWatch) { + // If the most recently dirty watcher is now clean, short circuit since the remaining watchers + // have already been tested. + dirty = false; + break traverseScopesLoop; + } + } + } catch (e) { + clearPhase(); + $exceptionHandler(e); + } + } + } - if (isUndefined(eventSupport[event])) { - var divElm = $window.document.createElement('div'); - eventSupport[event] = 'on' + event in divElm; - } + // Insanity Warning: scope depth-first traversal + // yes, this code is a bit crazy, but it works and we have tests to prove it! + // this piece should be kept in sync with the traversal in $broadcast + if (!(next = (current.$$childHead || + (current !== target && current.$$nextSibling)))) { + while(current !== target && !(next = current.$$nextSibling)) { + current = current.$parent; + } + } + } while ((current = next)); - return eventSupport[event]; - }, - // TODO(i): currently there is no way to feature detect CSP without triggering alerts - csp: false - }; - }]; -} + // `break traverseScopesLoop;` takes us to here -/** - * @ngdoc object - * @name ng.$window - * - * @description - * A reference to the browser's `window` object. While `window` - * is globally available in JavaScript, it causes testability problems, because - * it is a global variable. In angular we always refer to it through the - * `$window` service, so it may be overriden, removed or mocked for testing. - * - * All expressions are evaluated with respect to current scope so they don't - * suffer from window globality. - * - * @example - - - -
- - -
-
- - it('should display the greeting in the input box', function() { - input('greeting').enter('Hello, E2E Tests'); - // If we click the button it will block the test runner - // element(':button').click(); - }); - -
- */ -function $WindowProvider(){ - this.$get = valueFn(window); -} + if((dirty || asyncQueue.length) && !(ttl--)) { + clearPhase(); + throw $rootScopeMinErr('infdig', + '{0} $digest() iterations reached. Aborting!\n' + + 'Watchers fired in the last 5 iterations: {1}', + TTL, toJson(watchLog)); + } -/** - * Parse headers into key value object - * - * @param {string} headers Raw headers as a string - * @returns {Object} Parsed headers as key value object - */ -function parseHeaders(headers) { - var parsed = {}, key, val, i; + } while (dirty || asyncQueue.length); - if (!headers) return parsed; + clearPhase(); - forEach(headers.split('\n'), function(line) { - i = line.indexOf(':'); - key = lowercase(trim(line.substr(0, i))); - val = trim(line.substr(i + 1)); + while(postDigestQueue.length) { + try { + postDigestQueue.shift()(); + } catch (e) { + $exceptionHandler(e); + } + } + }, - if (key) { - if (parsed[key]) { - parsed[key] += ', ' + val; - } else { - parsed[key] = val; - } - } - }); - return parsed; -} + /** + * @ngdoc event + * @name $rootScope.Scope#$destroy + * @eventType broadcast on scope being destroyed + * + * @description + * Broadcasted when a scope and its children are being destroyed. + * + * Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to + * clean up DOM bindings before an element is removed from the DOM. + */ + + /** + * @ngdoc method + * @name $rootScope.Scope#$destroy + * @kind function + * + * @description + * Removes the current scope (and all of its children) from the parent scope. Removal implies + * that calls to {@link ng.$rootScope.Scope#$digest $digest()} will no longer + * propagate to the current scope and its children. Removal also implies that the current + * scope is eligible for garbage collection. + * + * The `$destroy()` is usually used by directives such as + * {@link ng.directive:ngRepeat ngRepeat} for managing the + * unrolling of the loop. + * + * Just before a scope is destroyed, a `$destroy` event is broadcasted on this scope. + * Application code can register a `$destroy` event handler that will give it a chance to + * perform any necessary cleanup. + * + * Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to + * clean up DOM bindings before an element is removed from the DOM. + */ + $destroy: function() { + // we can't destroy the root scope or a scope that has been already destroyed + if (this.$$destroyed) return; + var parent = this.$parent; + this.$broadcast('$destroy'); + this.$$destroyed = true; + if (this === $rootScope) return; -/** - * Returns a function that provides access to parsed headers. - * - * Headers are lazy parsed when first requested. - * @see parseHeaders - * - * @param {(string|Object)} headers Headers to provide access to. - * @returns {function(string=)} Returns a getter function which if called with: - * - * - if called with single an argument returns a single header value or null - * - if called with no arguments returns an object containing all headers. - */ -function headersGetter(headers) { - var headersObj = isObject(headers) ? headers : undefined; + forEach(this.$$listenerCount, bind(null, decrementListenerCount, this)); - return function(name) { - if (!headersObj) headersObj = parseHeaders(headers); + // sever all the references to parent scopes (after this cleanup, the current scope should + // not be retained by any of our references and should be eligible for garbage collection) + if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling; + if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling; + if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling; + if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling; - if (name) { - return headersObj[lowercase(name)] || null; - } - return headersObj; - }; -} + // All of the code below is bogus code that works around V8's memory leak via optimized code + // and inline caches. + // + // see: + // - https://code.google.com/p/v8/issues/detail?id=2073#c26 + // - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909 + // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 + this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead = + this.$$childTail = this.$root = null; -/** - * Chain all given functions - * - * This function is used for both request and response transforming - * - * @param {*} data Data to transform. - * @param {function(string=)} headers Http headers getter fn. - * @param {(function|Array.)} fns Function or an array of functions. - * @returns {*} Transformed data. - */ -function transformData(data, headers, fns) { - if (isFunction(fns)) - return fns(data, headers); + // don't reset these to null in case some async task tries to register a listener/watch/task + this.$$listeners = {}; + this.$$watchers = this.$$asyncQueue = this.$$postDigestQueue = []; - forEach(fns, function(fn) { - data = fn(data, headers); - }); + // prevent NPEs since these methods have references to properties we nulled out + this.$destroy = this.$digest = this.$apply = noop; + this.$on = this.$watch = function() { return noop; }; + }, - return data; -} + /** + * @ngdoc method + * @name $rootScope.Scope#$eval + * @kind function + * + * @description + * Executes the `expression` on the current scope and returns the result. Any exceptions in + * the expression are propagated (uncaught). This is useful when evaluating Angular + * expressions. + * + * # Example + * ```js + var scope = ng.$rootScope.Scope(); + scope.a = 1; + scope.b = 2; + expect(scope.$eval('a+b')).toEqual(3); + expect(scope.$eval(function(scope){ return scope.a + scope.b; })).toEqual(3); + * ``` + * + * @param {(string|function())=} expression An angular expression to be executed. + * + * - `string`: execute using the rules as defined in {@link guide/expression expression}. + * - `function(scope)`: execute the function with the current `scope` parameter. + * + * @param {(object)=} locals Local variables object, useful for overriding values in scope. + * @returns {*} The result of evaluating the expression. + */ + $eval: function(expr, locals) { + return $parse(expr)(this, locals); + }, -function isSuccess(status) { - return 200 <= status && status < 300; -} + /** + * @ngdoc method + * @name $rootScope.Scope#$evalAsync + * @kind function + * + * @description + * Executes the expression on the current scope at a later point in time. + * + * The `$evalAsync` makes no guarantees as to when the `expression` will be executed, only + * that: + * + * - it will execute after the function that scheduled the evaluation (preferably before DOM + * rendering). + * - at least one {@link ng.$rootScope.Scope#$digest $digest cycle} will be performed after + * `expression` execution. + * + * Any exceptions from the execution of the expression are forwarded to the + * {@link ng.$exceptionHandler $exceptionHandler} service. + * + * __Note:__ if this function is called outside of a `$digest` cycle, a new `$digest` cycle + * will be scheduled. However, it is encouraged to always call code that changes the model + * from within an `$apply` call. That includes code evaluated via `$evalAsync`. + * + * @param {(string|function())=} expression An angular expression to be executed. + * + * - `string`: execute using the rules as defined in {@link guide/expression expression}. + * - `function(scope)`: execute the function with the current `scope` parameter. + * + */ + $evalAsync: function(expr) { + // if we are outside of an $digest loop and this is the first time we are scheduling async + // task also schedule async auto-flush + if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) { + $browser.defer(function() { + if ($rootScope.$$asyncQueue.length) { + $rootScope.$digest(); + } + }); + } + this.$$asyncQueue.push({scope: this, expression: expr}); + }, -function $HttpProvider() { - var JSON_START = /^\s*(\[|\{[^\{])/, - JSON_END = /[\}\]]\s*$/, - PROTECTION_PREFIX = /^\)\]\}',?\n/; + $$postDigest : function(fn) { + this.$$postDigestQueue.push(fn); + }, - var $config = this.defaults = { - // transform incoming response data - transformResponse: [function(data) { - if (isString(data)) { - // strip json vulnerability protection prefix - data = data.replace(PROTECTION_PREFIX, ''); - if (JSON_START.test(data) && JSON_END.test(data)) - data = fromJson(data, true); - } - return data; - }], + /** + * @ngdoc method + * @name $rootScope.Scope#$apply + * @kind function + * + * @description + * `$apply()` is used to execute an expression in angular from outside of the angular + * framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). + * Because we are calling into the angular framework we need to perform proper scope life + * cycle of {@link ng.$exceptionHandler exception handling}, + * {@link ng.$rootScope.Scope#$digest executing watches}. + * + * ## Life cycle + * + * # Pseudo-Code of `$apply()` + * ```js + function $apply(expr) { + try { + return $eval(expr); + } catch (e) { + $exceptionHandler(e); + } finally { + $root.$digest(); + } + } + * ``` + * + * + * Scope's `$apply()` method transitions through the following stages: + * + * 1. The {@link guide/expression expression} is executed using the + * {@link ng.$rootScope.Scope#$eval $eval()} method. + * 2. Any exceptions from the execution of the expression are forwarded to the + * {@link ng.$exceptionHandler $exceptionHandler} service. + * 3. The {@link ng.$rootScope.Scope#$watch watch} listeners are fired immediately after the + * expression was executed using the {@link ng.$rootScope.Scope#$digest $digest()} method. + * + * + * @param {(string|function())=} exp An angular expression to be executed. + * + * - `string`: execute using the rules as defined in {@link guide/expression expression}. + * - `function(scope)`: execute the function with current `scope` parameter. + * + * @returns {*} The result of evaluating the expression. + */ + $apply: function(expr) { + try { + beginPhase('$apply'); + return this.$eval(expr); + } catch (e) { + $exceptionHandler(e); + } finally { + clearPhase(); + try { + $rootScope.$digest(); + } catch (e) { + $exceptionHandler(e); + throw e; + } + } + }, + + /** + * @ngdoc method + * @name $rootScope.Scope#$on + * @kind function + * + * @description + * Listens on events of a given type. See {@link ng.$rootScope.Scope#$emit $emit} for + * discussion of event life cycle. + * + * The event listener function format is: `function(event, args...)`. The `event` object + * passed into the listener has the following attributes: + * + * - `targetScope` - `{Scope}`: the scope on which the event was `$emit`-ed or + * `$broadcast`-ed. + * - `currentScope` - `{Scope}`: the current scope which is handling the event. + * - `name` - `{string}`: name of the event. + * - `stopPropagation` - `{function=}`: calling `stopPropagation` function will cancel + * further event propagation (available only for events that were `$emit`-ed). + * - `preventDefault` - `{function}`: calling `preventDefault` sets `defaultPrevented` flag + * to true. + * - `defaultPrevented` - `{boolean}`: true if `preventDefault` was called. + * + * @param {string} name Event name to listen on. + * @param {function(event, ...args)} listener Function to call when the event is emitted. + * @returns {function()} Returns a deregistration function for this listener. + */ + $on: function(name, listener) { + var namedListeners = this.$$listeners[name]; + if (!namedListeners) { + this.$$listeners[name] = namedListeners = []; + } + namedListeners.push(listener); - // transform outgoing request data - transformRequest: [function(d) { - return isObject(d) && !isFile(d) ? toJson(d) : d; - }], + var current = this; + do { + if (!current.$$listenerCount[name]) { + current.$$listenerCount[name] = 0; + } + current.$$listenerCount[name]++; + } while ((current = current.$parent)); - // default headers - headers: { - common: { - 'Accept': 'application/json, text/plain, */*', - 'X-Requested-With': 'XMLHttpRequest' + var self = this; + return function() { + var indexOfListener = indexOf(namedListeners, listener); + if (indexOfListener !== -1) { + namedListeners[indexOfListener] = null; + decrementListenerCount(self, 1, name); + } + }; }, - post: {'Content-Type': 'application/json;charset=utf-8'}, - put: {'Content-Type': 'application/json;charset=utf-8'} - } - }; - - var providerResponseInterceptors = this.responseInterceptors = []; - - this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', - function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { - var defaultCache = $cacheFactory('$http'), - responseInterceptors = []; - forEach(providerResponseInterceptors, function(interceptor) { - responseInterceptors.push( - isString(interceptor) - ? $injector.get(interceptor) - : $injector.invoke(interceptor) - ); - }); - - - /** - * @ngdoc function - * @name ng.$http - * @requires $httpBackend - * @requires $browser - * @requires $cacheFactory - * @requires $rootScope - * @requires $q - * @requires $injector - * - * @description - * The `$http` service is a core Angular service that facilitates communication with the remote - * HTTP servers via the browser's {@link https://developer.mozilla.org/en/xmlhttprequest - * XMLHttpRequest} object or via {@link http://en.wikipedia.org/wiki/JSONP JSONP}. - * - * For unit testing applications that use `$http` service, see - * {@link ngMock.$httpBackend $httpBackend mock}. - * - * For a higher level of abstraction, please check out the {@link ngResource.$resource - * $resource} service. - * - * The $http API is based on the {@link ng.$q deferred/promise APIs} exposed by - * the $q service. While for simple usage patterns this doesn't matter much, for advanced usage - * it is important to familiarize yourself with these APIs and the guarantees they provide. - * - * - * # General usage - * The `$http` service is a function which takes a single argument — a configuration object — - * that is used to generate an HTTP request and returns a {@link ng.$q promise} - * with two $http specific methods: `success` and `error`. - * - *
-     *   $http({method: 'GET', url: '/someUrl'}).
-     *     success(function(data, status, headers, config) {
-     *       // this callback will be called asynchronously
-     *       // when the response is available
-     *     }).
-     *     error(function(data, status, headers, config) {
-     *       // called asynchronously if an error occurs
-     *       // or server returns response with an error status.
-     *     });
-     * 
- * - * Since the returned value of calling the $http function is a `promise`, you can also use - * the `then` method to register callbacks, and these callbacks will receive a single argument – - * an object representing the response. See the API signature and type info below for more - * details. - * - * A response status code between 200 and 299 is considered a success status and - * will result in the success callback being called. Note that if the response is a redirect, - * XMLHttpRequest will transparently follow it, meaning that the error callback will not be - * called for such responses. - * - * # Shortcut methods - * - * Since all invocations of the $http service require passing in an HTTP method and URL, and - * POST/PUT requests require request data to be provided as well, shortcut methods - * were created: - * - *
-     *   $http.get('/someUrl').success(successCallback);
-     *   $http.post('/someUrl', data).success(successCallback);
-     * 
- * - * Complete list of shortcut methods: - * - * - {@link ng.$http#get $http.get} - * - {@link ng.$http#head $http.head} - * - {@link ng.$http#post $http.post} - * - {@link ng.$http#put $http.put} - * - {@link ng.$http#delete $http.delete} - * - {@link ng.$http#jsonp $http.jsonp} - * - * - * # Setting HTTP Headers - * - * The $http service will automatically add certain HTTP headers to all requests. These defaults - * can be fully configured by accessing the `$httpProvider.defaults.headers` configuration - * object, which currently contains this default configuration: - * - * - `$httpProvider.defaults.headers.common` (headers that are common for all requests): - * - `Accept: application/json, text/plain, * / *` - * - `X-Requested-With: XMLHttpRequest` - * - `$httpProvider.defaults.headers.post`: (header defaults for POST requests) - * - `Content-Type: application/json` - * - `$httpProvider.defaults.headers.put` (header defaults for PUT requests) - * - `Content-Type: application/json` - * - * To add or overwrite these defaults, simply add or remove a property from these configuration - * objects. To add headers for an HTTP method other than POST or PUT, simply add a new object - * with the lowercased HTTP method name as the key, e.g. - * `$httpProvider.defaults.headers.get['My-Header']='value'`. - * - * Additionally, the defaults can be set at runtime via the `$http.defaults` object in the same - * fashion. - * - * - * # Transforming Requests and Responses - * - * Both requests and responses can be transformed using transform functions. By default, Angular - * applies these transformations: - * - * Request transformations: - * - * - If the `data` property of the request configuration object contains an object, serialize it into - * JSON format. - * - * Response transformations: - * - * - If XSRF prefix is detected, strip it (see Security Considerations section below). - * - If JSON response is detected, deserialize it using a JSON parser. - * - * To globally augment or override the default transforms, modify the `$httpProvider.defaults.transformRequest` and - * `$httpProvider.defaults.transformResponse` properties. These properties are by default an - * array of transform functions, which allows you to `push` or `unshift` a new transformation function into the - * transformation chain. You can also decide to completely override any default transformations by assigning your - * transformation functions to these properties directly without the array wrapper. - * - * Similarly, to locally override the request/response transforms, augment the `transformRequest` and/or - * `transformResponse` properties of the configuration object passed into `$http`. - * - * - * # Caching - * - * To enable caching, set the configuration property `cache` to `true`. When the cache is - * enabled, `$http` stores the response from the server in local cache. Next time the - * response is served from the cache without sending a request to the server. - * - * Note that even if the response is served from cache, delivery of the data is asynchronous in - * the same way that real requests are. - * - * If there are multiple GET requests for the same URL that should be cached using the same - * cache, but the cache is not populated yet, only one request to the server will be made and - * the remaining requests will be fulfilled using the response from the first request. - * - * - * # Response interceptors - * - * Before you start creating interceptors, be sure to understand the - * {@link ng.$q $q and deferred/promise APIs}. - * - * For purposes of global error handling, authentication or any kind of synchronous or - * asynchronous preprocessing of received responses, it is desirable to be able to intercept - * responses for http requests before they are handed over to the application code that - * initiated these requests. The response interceptors leverage the {@link ng.$q - * promise apis} to fulfil this need for both synchronous and asynchronous preprocessing. - * - * The interceptors are service factories that are registered with the $httpProvider by - * adding them to the `$httpProvider.responseInterceptors` array. The factory is called and - * injected with dependencies (if specified) and returns the interceptor — a function that - * takes a {@link ng.$q promise} and returns the original or a new promise. - * - *
-     *   // register the interceptor as a service
-     *   $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) {
-     *     return function(promise) {
-     *       return promise.then(function(response) {
-     *         // do something on success
-     *       }, function(response) {
-     *         // do something on error
-     *         if (canRecover(response)) {
-     *           return responseOrNewPromise
-     *         }
-     *         return $q.reject(response);
-     *       });
-     *     }
-     *   });
-     *
-     *   $httpProvider.responseInterceptors.push('myHttpInterceptor');
-     *
-     *
-     *   // register the interceptor via an anonymous factory
-     *   $httpProvider.responseInterceptors.push(function($q, dependency1, dependency2) {
-     *     return function(promise) {
-     *       // same as above
-     *     }
-     *   });
-     * 
- * - * - * # Security Considerations - * - * When designing web applications, consider security threats from: - * - * - {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx - * JSON vulnerability} - * - {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF} - * - * Both server and the client must cooperate in order to eliminate these threats. Angular comes - * pre-configured with strategies that address these issues, but for this to work backend server - * cooperation is required. - * - * ## JSON Vulnerability Protection - * - * A {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx - * JSON vulnerability} allows third party website to turn your JSON resource URL into - * {@link http://en.wikipedia.org/wiki/JSONP JSONP} request under some conditions. To - * counter this your server can prefix all JSON requests with following string `")]}',\n"`. - * Angular will automatically strip the prefix before processing it as JSON. - * - * For example if your server needs to return: - *
-     * ['one','two']
-     * 
- * - * which is vulnerable to attack, your server can return: - *
-     * )]}',
-     * ['one','two']
-     * 
- * - * Angular will strip the prefix, before processing the JSON. - * - * - * ## Cross Site Request Forgery (XSRF) Protection - * - * {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF} is a technique by which - * an unauthorized site can gain your user's private data. Angular provides a mechanism - * to counter XSRF. When performing XHR requests, the $http service reads a token from a cookie - * called `XSRF-TOKEN` and sets it as the HTTP header `X-XSRF-TOKEN`. Since only JavaScript that - * runs on your domain could read the cookie, your server can be assured that the XHR came from - * JavaScript running on your domain. - * - * To take advantage of this, your server needs to set a token in a JavaScript readable session - * cookie called `XSRF-TOKEN` on the first HTTP GET request. On subsequent XHR requests the - * server can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure - * that only JavaScript running on your domain could have sent the request. The token must be - * unique for each user and must be verifiable by the server (to prevent the JavaScript from making - * up its own tokens). We recommend that the token is a digest of your site's authentication - * cookie with a {@link https://en.wikipedia.org/wiki/Salt_(cryptography) salt} for added security. - * - * - * @param {object} config Object describing the request to be made and how it should be - * processed. The object has following properties: - * - * - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) - * - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. - * - **params** – `{Object.}` – Map of strings or objects which will be turned to - * `?key1=value1&key2=value2` after the url. If the value is not a string, it will be JSONified. - * - **data** – `{string|Object}` – Data to be sent as the request message data. - * - **headers** – `{Object}` – Map of strings representing HTTP headers to send to the server. - * - **transformRequest** – `{function(data, headersGetter)|Array.}` – - * transform function or an array of such functions. The transform function takes the http - * request body and headers and returns its transformed (typically serialized) version. - * - **transformResponse** – `{function(data, headersGetter)|Array.}` – - * transform function or an array of such functions. The transform function takes the http - * response body and headers and returns its transformed (typically deserialized) version. - * - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the - * GET request, otherwise if a cache instance built with - * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for - * caching. - * - **timeout** – `{number}` – timeout in milliseconds. - * - **withCredentials** - `{boolean}` - whether to to set the `withCredentials` flag on the - * XHR object. See {@link https://developer.mozilla.org/en/http_access_control#section_5 - * requests with credentials} for more information. - * - * @returns {HttpPromise} Returns a {@link ng.$q promise} object with the - * standard `then` method and two http specific methods: `success` and `error`. The `then` - * method takes two arguments a success and an error callback which will be called with a - * response object. The `success` and `error` methods take a single argument - a function that - * will be called when the request succeeds or fails respectively. The arguments passed into - * these functions are destructured representation of the response object passed into the - * `then` method. The response object has these properties: - * - * - **data** – `{string|Object}` – The response body transformed with the transform functions. - * - **status** – `{number}` – HTTP status code of the response. - * - **headers** – `{function([headerName])}` – Header getter function. - * - **config** – `{Object}` – The configuration object that was used to generate the request. - * - * @property {Array.} pendingRequests Array of config objects for currently pending - * requests. This is primarily meant to be used for debugging purposes. - * - * - * @example - - -
- - -
- - - -
http status code: {{status}}
-
http response data: {{data}}
-
-
- - function FetchCtrl($scope, $http, $templateCache) { - $scope.method = 'GET'; - $scope.url = 'http-hello.html'; - - $scope.fetch = function() { - $scope.code = null; - $scope.response = null; - - $http({method: $scope.method, url: $scope.url, cache: $templateCache}). - success(function(data, status) { - $scope.status = status; - $scope.data = data; - }). - error(function(data, status) { - $scope.data = data || "Request failed"; - $scope.status = status; - }); - }; + /** + * @ngdoc method + * @name $rootScope.Scope#$emit + * @kind function + * + * @description + * Dispatches an event `name` upwards through the scope hierarchy notifying the + * registered {@link ng.$rootScope.Scope#$on} listeners. + * + * The event life cycle starts at the scope on which `$emit` was called. All + * {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get + * notified. Afterwards, the event traverses upwards toward the root scope and calls all + * registered listeners along the way. The event will stop propagating if one of the listeners + * cancels it. + * + * Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed + * onto the {@link ng.$exceptionHandler $exceptionHandler} service. + * + * @param {string} name Event name to emit. + * @param {...*} args Optional one or more arguments which will be passed onto the event listeners. + * @return {Object} Event object (see {@link ng.$rootScope.Scope#$on}). + */ + $emit: function(name, args) { + var empty = [], + namedListeners, + scope = this, + stopPropagation = false, + event = { + name: name, + targetScope: scope, + stopPropagation: function() {stopPropagation = true;}, + preventDefault: function() { + event.defaultPrevented = true; + }, + defaultPrevented: false + }, + listenerArgs = concat([event], arguments, 1), + i, length; - $scope.updateModel = function(method, url) { - $scope.method = method; - $scope.url = url; - }; + do { + namedListeners = scope.$$listeners[name] || empty; + event.currentScope = scope; + for (i=0, length=namedListeners.length; i - - Hello, $http! - - - it('should make an xhr GET request', function() { - element(':button:contains("Sample GET")').click(); - element(':button:contains("fetch")').click(); - expect(binding('status')).toBe('200'); - expect(binding('data')).toMatch(/Hello, \$http!/); - }); + //if any listener on the current scope stops propagation, prevent bubbling + if (stopPropagation) return event; + //traverse upwards + scope = scope.$parent; + } while (scope); - it('should make a JSONP request to angularjs.org', function() { - element(':button:contains("Sample JSONP")').click(); - element(':button:contains("fetch")').click(); - expect(binding('status')).toBe('200'); - expect(binding('data')).toMatch(/Super Hero!/); - }); + return event; + }, - it('should make JSONP request to invalid URL and invoke the error handler', - function() { - element(':button:contains("Invalid JSONP")').click(); - element(':button:contains("fetch")').click(); - expect(binding('status')).toBe('0'); - expect(binding('data')).toBe('Request failed'); - }); - -
+ + /** + * @ngdoc method + * @name $rootScope.Scope#$broadcast + * @kind function + * + * @description + * Dispatches an event `name` downwards to all child scopes (and their children) notifying the + * registered {@link ng.$rootScope.Scope#$on} listeners. + * + * The event life cycle starts at the scope on which `$broadcast` was called. All + * {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get + * notified. Afterwards, the event propagates to all direct and indirect scopes of the current + * scope and calls all registered listeners along the way. The event cannot be canceled. + * + * Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed + * onto the {@link ng.$exceptionHandler $exceptionHandler} service. + * + * @param {string} name Event name to broadcast. + * @param {...*} args Optional one or more arguments which will be passed onto the event listeners. + * @return {Object} Event object, see {@link ng.$rootScope.Scope#$on} + */ + $broadcast: function(name, args) { + var target = this, + current = target, + next = target, + event = { + name: name, + targetScope: target, + preventDefault: function() { + event.defaultPrevented = true; + }, + defaultPrevented: false + }, + listenerArgs = concat([event], arguments, 1), + listeners, i, length; + + //down while you can, then up and next sibling or up and next sibling until back at root + while ((current = next)) { + event.currentScope = current; + listeners = current.$$listeners[name] || []; + for (i=0, length = listeners.length; i= 8 ) { + normalizedVal = urlResolve(uri).href; + if (normalizedVal !== '' && !normalizedVal.match(regex)) { + return 'unsafe:'+normalizedVal; + } } + return uri; + }; + }; +} + +var $sceMinErr = minErr('$sce'); + +var SCE_CONTEXTS = { + HTML: 'html', + CSS: 'css', + URL: 'url', + // RESOURCE_URL is a subtype of URL used in contexts where a privileged resource is sourced from a + // url. (e.g. ng-include, script src, templateUrl) + RESOURCE_URL: 'resourceUrl', + JS: 'js' +}; + +// Helper functions follow. + +// Copied from: +// http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962 +// Prereq: s is a string. +function escapeForRegexp(s) { + return s.replace(/([-()\[\]{}+?*.$\^|,:# -1) { + throw $sceMinErr('iwcard', + 'Illegal sequence *** in string matcher. String: {0}', matcher); + } + matcher = escapeForRegexp(matcher). + replace('\\*\\*', '.*'). + replace('\\*', '[^:/.?&;]*'); + return new RegExp('^' + matcher + '$'); + } else if (isRegExp(matcher)) { + // The only other type of matcher allowed is a Regexp. + // Match entire URL / disallow partial matches. + // Flags are reset (i.e. no global, ignoreCase or multiline) + return new RegExp('^' + matcher.source + '$'); + } else { + throw $sceMinErr('imatcher', + 'Matchers may only be "self", string patterns or RegExp objects'); + } +} + + +function adjustMatchers(matchers) { + var adjustedMatchers = []; + if (isDefined(matchers)) { + forEach(matchers, function(matcher) { + adjustedMatchers.push(adjustMatcher(matcher)); + }); + } + return adjustedMatchers; +} + + +/** + * @ngdoc service + * @name $sceDelegate + * @kind function + * + * @description + * + * `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict + * Contextual Escaping (SCE)} services to AngularJS. + * + * Typically, you would configure or override the {@link ng.$sceDelegate $sceDelegate} instead of + * the `$sce` service to customize the way Strict Contextual Escaping works in AngularJS. This is + * because, while the `$sce` provides numerous shorthand methods, etc., you really only need to + * override 3 core functions (`trustAs`, `getTrusted` and `valueOf`) to replace the way things + * work because `$sce` delegates to `$sceDelegate` for these operations. + * + * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} to configure this service. + * + * The default instance of `$sceDelegate` should work out of the box with little pain. While you + * can override it completely to change the behavior of `$sce`, the common case would + * involve configuring the {@link ng.$sceDelegateProvider $sceDelegateProvider} instead by setting + * your own whitelists and blacklists for trusting URLs used for loading AngularJS resources such as + * templates. Refer {@link ng.$sceDelegateProvider#resourceUrlWhitelist + * $sceDelegateProvider.resourceUrlWhitelist} and {@link + * ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} + */ + +/** + * @ngdoc provider + * @name $sceDelegateProvider + * @description + * + * The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate + * $sceDelegate} service. This allows one to get/set the whitelists and blacklists used to ensure + * that the URLs used for sourcing Angular templates are safe. Refer {@link + * ng.$sceDelegateProvider#resourceUrlWhitelist $sceDelegateProvider.resourceUrlWhitelist} and + * {@link ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} + * + * For the general details about this service in Angular, read the main page for {@link ng.$sce + * Strict Contextual Escaping (SCE)}. + * + * **Example**: Consider the following case. + * + * - your app is hosted at url `http://myapp.example.com/` + * - but some of your templates are hosted on other domains you control such as + * `http://srv01.assets.example.com/`,  `http://srv02.assets.example.com/`, etc. + * - and you have an open redirect at `http://myapp.example.com/clickThru?...`. + * + * Here is what a secure configuration for this scenario might look like: + * + * ``` + * angular.module('myApp', []).config(function($sceDelegateProvider) { + * $sceDelegateProvider.resourceUrlWhitelist([ + * // Allow same origin resource loads. + * 'self', + * // Allow loading from our assets domain. Notice the difference between * and **. + * 'http://srv*.assets.example.com/**' + * ]); + * + * // The blacklist overrides the whitelist so the open redirect here is blocked. + * $sceDelegateProvider.resourceUrlBlacklist([ + * 'http://myapp.example.com/clickThru**' + * ]); + * }); + * ``` + */ + +function $SceDelegateProvider() { + this.SCE_CONTEXTS = SCE_CONTEXTS; + + // Resource URLs can also be trusted by policy. + var resourceUrlWhitelist = ['self'], + resourceUrlBlacklist = []; + + /** + * @ngdoc method + * @name $sceDelegateProvider#resourceUrlWhitelist + * @kind function + * + * @param {Array=} whitelist When provided, replaces the resourceUrlWhitelist with the value + * provided. This must be an array or null. A snapshot of this array is used so further + * changes to the array are ignored. + * + * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items + * allowed in this array. + * + * Note: **an empty whitelist array will block all URLs**! + * + * @return {Array} the currently set whitelist array. + * + * The **default value** when no whitelist has been explicitly set is `['self']` allowing only + * same origin resource requests. + * + * @description + * Sets/Gets the whitelist of trusted resource URLs. + */ + this.resourceUrlWhitelist = function (value) { + if (arguments.length) { + resourceUrlWhitelist = adjustMatchers(value); + } + return resourceUrlWhitelist; + }; - // send request - promise = sendReq(config, reqData, reqHeaders); + /** + * @ngdoc method + * @name $sceDelegateProvider#resourceUrlBlacklist + * @kind function + * + * @param {Array=} blacklist When provided, replaces the resourceUrlBlacklist with the value + * provided. This must be an array or null. A snapshot of this array is used so further + * changes to the array are ignored. + * + * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items + * allowed in this array. + * + * The typical usage for the blacklist is to **block + * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as + * these would otherwise be trusted but actually return content from the redirected domain. + * + * Finally, **the blacklist overrides the whitelist** and has the final say. + * + * @return {Array} the currently set blacklist array. + * + * The **default value** when no whitelist has been explicitly set is the empty array (i.e. there + * is no blacklist.) + * + * @description + * Sets/Gets the blacklist of trusted resource URLs. + */ + this.resourceUrlBlacklist = function (value) { + if (arguments.length) { + resourceUrlBlacklist = adjustMatchers(value); + } + return resourceUrlBlacklist; + }; - // transform future response - promise = promise.then(transformResponse, transformResponse); + this.$get = ['$injector', function($injector) { - // apply interceptors - forEach(responseInterceptors, function(interceptor) { - promise = interceptor(promise); - }); + var htmlSanitizer = function htmlSanitizer(html) { + throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); + }; - promise.success = function(fn) { - promise.then(function(response) { - fn(response.data, response.status, response.headers, config); - }); - return promise; - }; + if ($injector.has('$sanitize')) { + htmlSanitizer = $injector.get('$sanitize'); + } - promise.error = function(fn) { - promise.then(null, function(response) { - fn(response.data, response.status, response.headers, config); - }); - return promise; - }; - return promise; + function matchUrl(matcher, parsedUrl) { + if (matcher === 'self') { + return urlIsSameOrigin(parsedUrl); + } else { + // definitely a regex. See adjustMatchers() + return !!matcher.exec(parsedUrl.href); + } + } - function transformResponse(response) { - // make a copy since the response must be cacheable - var resp = extend({}, response, { - data: transformData(response.data, response.headers, respTransformFn) - }); - return (isSuccess(response.status)) - ? resp - : $q.reject(resp); + function isResourceUrlAllowedByPolicy(url) { + var parsedUrl = urlResolve(url.toString()); + var i, n, allowed = false; + // Ensure that at least one item from the whitelist allows this url. + for (i = 0, n = resourceUrlWhitelist.length; i < n; i++) { + if (matchUrl(resourceUrlWhitelist[i], parsedUrl)) { + allowed = true; + break; + } + } + if (allowed) { + // Ensure that no item from the blacklist blocked this url. + for (i = 0, n = resourceUrlBlacklist.length; i < n; i++) { + if (matchUrl(resourceUrlBlacklist[i], parsedUrl)) { + allowed = false; + break; + } + } } + return allowed; } - $http.pendingRequests = []; + function generateHolderType(Base) { + var holderType = function TrustedValueHolderType(trustedValue) { + this.$$unwrapTrustedValue = function() { + return trustedValue; + }; + }; + if (Base) { + holderType.prototype = new Base(); + } + holderType.prototype.valueOf = function sceValueOf() { + return this.$$unwrapTrustedValue(); + }; + holderType.prototype.toString = function sceToString() { + return this.$$unwrapTrustedValue().toString(); + }; + return holderType; + } - /** - * @ngdoc method - * @name ng.$http#get - * @methodOf ng.$http - * - * @description - * Shortcut method to perform `GET` request. - * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object - */ + var trustedValueHolderBase = generateHolderType(), + byType = {}; - /** - * @ngdoc method - * @name ng.$http#delete - * @methodOf ng.$http - * - * @description - * Shortcut method to perform `DELETE` request. - * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object - */ + byType[SCE_CONTEXTS.HTML] = generateHolderType(trustedValueHolderBase); + byType[SCE_CONTEXTS.CSS] = generateHolderType(trustedValueHolderBase); + byType[SCE_CONTEXTS.URL] = generateHolderType(trustedValueHolderBase); + byType[SCE_CONTEXTS.JS] = generateHolderType(trustedValueHolderBase); + byType[SCE_CONTEXTS.RESOURCE_URL] = generateHolderType(byType[SCE_CONTEXTS.URL]); /** * @ngdoc method - * @name ng.$http#head - * @methodOf ng.$http + * @name $sceDelegate#trustAs * * @description - * Shortcut method to perform `HEAD` request. + * Returns an object that is trusted by angular for use in specified strict + * contextual escaping contexts (such as ng-bind-html, ng-include, any src + * attribute interpolation, any dom event binding attribute interpolation + * such as for onclick, etc.) that uses the provided value. + * See {@link ng.$sce $sce} for enabling strict contextual escaping. * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object + * @param {string} type The kind of context in which this value is safe for use. e.g. url, + * resourceUrl, html, js and css. + * @param {*} value The value that that should be considered trusted/safe. + * @returns {*} A value that can be used to stand in for the provided `value` in places + * where Angular expects a $sce.trustAs() return value. */ + function trustAs(type, trustedValue) { + var Constructor = (byType.hasOwnProperty(type) ? byType[type] : null); + if (!Constructor) { + throw $sceMinErr('icontext', + 'Attempted to trust a value in invalid context. Context: {0}; Value: {1}', + type, trustedValue); + } + if (trustedValue === null || trustedValue === undefined || trustedValue === '') { + return trustedValue; + } + // All the current contexts in SCE_CONTEXTS happen to be strings. In order to avoid trusting + // mutable objects, we ensure here that the value passed in is actually a string. + if (typeof trustedValue !== 'string') { + throw $sceMinErr('itype', + 'Attempted to trust a non-string value in a content requiring a string: Context: {0}', + type); + } + return new Constructor(trustedValue); + } /** * @ngdoc method - * @name ng.$http#jsonp - * @methodOf ng.$http + * @name $sceDelegate#valueOf * * @description - * Shortcut method to perform `JSONP` request. - * - * @param {string} url Relative or absolute URL specifying the destination of the request. - * Should contain `JSON_CALLBACK` string. - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object - */ - createShortMethods('get', 'delete', 'head', 'jsonp'); - - /** - * @ngdoc method - * @name ng.$http#post - * @methodOf ng.$http + * If the passed parameter had been returned by a prior call to {@link ng.$sceDelegate#trustAs + * `$sceDelegate.trustAs`}, returns the value that had been passed to {@link + * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. * - * @description - * Shortcut method to perform `POST` request. + * If the passed parameter is not a value that had been returned by {@link + * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}, returns it as-is. * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {*} data Request content - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object + * @param {*} value The result of a prior {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} + * call or anything else. + * @returns {*} The `value` that was originally provided to {@link ng.$sceDelegate#trustAs + * `$sceDelegate.trustAs`} if `value` is the result of such a call. Otherwise, returns + * `value` unchanged. */ + function valueOf(maybeTrusted) { + if (maybeTrusted instanceof trustedValueHolderBase) { + return maybeTrusted.$$unwrapTrustedValue(); + } else { + return maybeTrusted; + } + } /** * @ngdoc method - * @name ng.$http#put - * @methodOf ng.$http + * @name $sceDelegate#getTrusted * * @description - * Shortcut method to perform `PUT` request. - * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {*} data Request content - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object - */ - createShortMethodsWithData('post', 'put'); - - /** - * @ngdoc property - * @name ng.$http#defaults - * @propertyOf ng.$http - * - * @description - * Runtime equivalent of the `$httpProvider.defaults` property. Allows configuration of - * default headers as well as request and response transformations. - * - * See "Setting HTTP Headers" and "Transforming Requests and Responses" sections above. - */ - $http.defaults = $config; - - - return $http; - - - function createShortMethods(names) { - forEach(arguments, function(name) { - $http[name] = function(url, config) { - return $http(extend(config || {}, { - method: name, - url: url - })); - }; - }); - } - - - function createShortMethodsWithData(name) { - forEach(arguments, function(name) { - $http[name] = function(url, data, config) { - return $http(extend(config || {}, { - method: name, - url: url, - data: data - })); - }; - }); - } - - - /** - * Makes the request. + * Takes the result of a {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} call and + * returns the originally supplied value if the queried context type is a supertype of the + * created type. If this condition isn't satisfied, throws an exception. * - * !!! ACCESSES CLOSURE VARS: - * $httpBackend, $config, $log, $rootScope, defaultCache, $http.pendingRequests + * @param {string} type The kind of context in which this value is to be used. + * @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#trustAs + * `$sceDelegate.trustAs`} call. + * @returns {*} The value the was originally provided to {@link ng.$sceDelegate#trustAs + * `$sceDelegate.trustAs`} if valid in this context. Otherwise, throws an exception. */ - function sendReq(config, reqData, reqHeaders) { - var deferred = $q.defer(), - promise = deferred.promise, - cache, - cachedResp, - url = buildUrl(config.url, config.params); - - $http.pendingRequests.push(config); - promise.then(removePendingReq, removePendingReq); - - - if (config.cache && config.method == 'GET') { - cache = isObject(config.cache) ? config.cache : defaultCache; - } - - if (cache) { - cachedResp = cache.get(url); - if (cachedResp) { - if (cachedResp.then) { - // cached request has already been sent, but there is no response yet - cachedResp.then(removePendingReq, removePendingReq); - return cachedResp; - } else { - // serving from cache - if (isArray(cachedResp)) { - resolvePromise(cachedResp[1], cachedResp[0], copy(cachedResp[2])); - } else { - resolvePromise(cachedResp, 200, {}); - } - } - } else { - // put the promise for the non-transformed response into cache as a placeholder - cache.put(url, promise); - } + function getTrusted(type, maybeTrusted) { + if (maybeTrusted === null || maybeTrusted === undefined || maybeTrusted === '') { + return maybeTrusted; } - - // if we won't have the response in cache, send the request to the backend - if (!cachedResp) { - $httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout, - config.withCredentials); + var constructor = (byType.hasOwnProperty(type) ? byType[type] : null); + if (constructor && maybeTrusted instanceof constructor) { + return maybeTrusted.$$unwrapTrustedValue(); } - - return promise; - - - /** - * Callback registered to $httpBackend(): - * - caches the response if desired - * - resolves the raw $http promise - * - calls $apply - */ - function done(status, response, headersString) { - if (cache) { - if (isSuccess(status)) { - cache.put(url, [status, response, parseHeaders(headersString)]); - } else { - // remove promise from the cache - cache.remove(url); - } + // If we get here, then we may only take one of two actions. + // 1. sanitize the value for the requested type, or + // 2. throw an exception. + if (type === SCE_CONTEXTS.RESOURCE_URL) { + if (isResourceUrlAllowedByPolicy(maybeTrusted)) { + return maybeTrusted; + } else { + throw $sceMinErr('insecurl', + 'Blocked loading resource from url not allowed by $sceDelegate policy. URL: {0}', + maybeTrusted.toString()); } - - resolvePromise(response, status, headersString); - $rootScope.$apply(); - } - - - /** - * Resolves the raw $http promise. - */ - function resolvePromise(response, status, headers) { - // normalize internal statuses to 0 - status = Math.max(status, 0); - - (isSuccess(status) ? deferred.resolve : deferred.reject)({ - data: response, - status: status, - headers: headersGetter(headers), - config: config - }); - } - - - function removePendingReq() { - var idx = indexOf($http.pendingRequests, config); - if (idx !== -1) $http.pendingRequests.splice(idx, 1); + } else if (type === SCE_CONTEXTS.HTML) { + return htmlSanitizer(maybeTrusted); } + throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); } - - function buildUrl(url, params) { - if (!params) return url; - var parts = []; - forEachSorted(params, function(value, key) { - if (value == null || value == undefined) return; - if (isObject(value)) { - value = toJson(value); - } - parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); - }); - return url + ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&'); - } - - + return { trustAs: trustAs, + getTrusted: getTrusted, + valueOf: valueOf }; }]; } -var XHR = window.XMLHttpRequest || function() { - try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e1) {} - try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e2) {} - try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {} - throw new Error("This browser does not support XMLHttpRequest."); -}; +/** + * @ngdoc provider + * @name $sceProvider + * @description + * + * The $sceProvider provider allows developers to configure the {@link ng.$sce $sce} service. + * - enable/disable Strict Contextual Escaping (SCE) in a module + * - override the default implementation with a custom delegate + * + * Read more about {@link ng.$sce Strict Contextual Escaping (SCE)}. + */ + +/* jshint maxlen: false*/ /** - * @ngdoc object - * @name ng.$httpBackend - * @requires $browser - * @requires $window - * @requires $document + * @ngdoc service + * @name $sce + * @kind function * * @description - * HTTP backend used by the {@link ng.$http service} that delegates to - * XMLHttpRequest object or JSONP and deals with browser incompatibilities. * - * You should never need to use this service directly, instead use the higher-level abstractions: - * {@link ng.$http $http} or {@link ngResource.$resource $resource}. + * `$sce` is a service that provides Strict Contextual Escaping services to AngularJS. + * + * # Strict Contextual Escaping + * + * Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain + * contexts to result in a value that is marked as safe to use for that context. One example of + * such a context is binding arbitrary html controlled by the user via `ng-bind-html`. We refer + * to these contexts as privileged or SCE contexts. + * + * As of version 1.2, Angular ships with SCE enabled by default. + * + * Note: When enabled (the default), IE8 in quirks mode is not supported. In this mode, IE8 allows + * one to execute arbitrary javascript by the use of the expression() syntax. Refer + * to learn more about them. + * You can ensure your document is in standards mode and not quirks mode by adding `` + * to the top of your HTML document. + * + * SCE assists in writing code in way that (a) is secure by default and (b) makes auditing for + * security vulnerabilities such as XSS, clickjacking, etc. a lot easier. + * + * Here's an example of a binding in a privileged context: + * + * ``` + * + *
+ * ``` + * + * Notice that `ng-bind-html` is bound to `userHtml` controlled by the user. With SCE + * disabled, this application allows the user to render arbitrary HTML into the DIV. + * In a more realistic example, one may be rendering user comments, blog articles, etc. via + * bindings. (HTML is just one example of a context where rendering user controlled input creates + * security vulnerabilities.) + * + * For the case of HTML, you might use a library, either on the client side, or on the server side, + * to sanitize unsafe HTML before binding to the value and rendering it in the document. + * + * How would you ensure that every place that used these types of bindings was bound to a value that + * was sanitized by your library (or returned as safe for rendering by your server?) How can you + * ensure that you didn't accidentally delete the line that sanitized the value, or renamed some + * properties/fields and forgot to update the binding to the sanitized value? + * + * To be secure by default, you want to ensure that any such bindings are disallowed unless you can + * determine that something explicitly says it's safe to use a value for binding in that + * context. You can then audit your code (a simple grep would do) to ensure that this is only done + * for those values that you can easily tell are safe - because they were received from your server, + * sanitized by your library, etc. You can organize your codebase to help with this - perhaps + * allowing only the files in a specific directory to do this. Ensuring that the internal API + * exposed by that code doesn't markup arbitrary values as safe then becomes a more manageable task. + * + * In the case of AngularJS' SCE service, one uses {@link ng.$sce#trustAs $sce.trustAs} + * (and shorthand methods such as {@link ng.$sce#trustAsHtml $sce.trustAsHtml}, etc.) to + * obtain values that will be accepted by SCE / privileged contexts. + * + * + * ## How does it work? + * + * In privileged contexts, directives and code will bind to the result of {@link ng.$sce#getTrusted + * $sce.getTrusted(context, value)} rather than to the value directly. Directives use {@link + * ng.$sce#parse $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the + * {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals. + * + * As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link + * ng.$sce#parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly + * simplified): + * + * ``` + * var ngBindHtmlDirective = ['$sce', function($sce) { + * return function(scope, element, attr) { + * scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function(value) { + * element.html(value || ''); + * }); + * }; + * }]; + * ``` + * + * ## Impact on loading templates + * + * This applies both to the {@link ng.directive:ngInclude `ng-include`} directive as well as + * `templateUrl`'s specified by {@link guide/directive directives}. + * + * By default, Angular only loads templates from the same domain and protocol as the application + * document. This is done by calling {@link ng.$sce#getTrustedResourceUrl + * $sce.getTrustedResourceUrl} on the template URL. To load templates from other domains and/or + * protocols, you may either either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist + * them} or {@link ng.$sce#trustAsResourceUrl wrap it} into a trusted value. + * + * *Please note*: + * The browser's + * [Same Origin Policy](https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest) + * and [Cross-Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) + * policy apply in addition to this and may further restrict whether the template is successfully + * loaded. This means that without the right CORS policy, loading templates from a different domain + * won't work on all browsers. Also, loading templates from `file://` URL does not work on some + * browsers. + * + * ## This feels like too much overhead for the developer? + * + * It's important to remember that SCE only applies to interpolation expressions. + * + * If your expressions are constant literals, they're automatically trusted and you don't need to + * call `$sce.trustAs` on them (remember to include the `ngSanitize` module) (e.g. + * `
`) just works. + * + * Additionally, `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them + * through {@link ng.$sce#getTrusted $sce.getTrusted}. SCE doesn't play a role here. + * + * The included {@link ng.$sceDelegate $sceDelegate} comes with sane defaults to allow you to load + * templates in `ng-include` from your application's domain without having to even know about SCE. + * It blocks loading templates from other domains or loading templates over http from an https + * served document. You can change these by setting your own custom {@link + * ng.$sceDelegateProvider#resourceUrlWhitelist whitelists} and {@link + * ng.$sceDelegateProvider#resourceUrlBlacklist blacklists} for matching such URLs. + * + * This significantly reduces the overhead. It is far easier to pay the small overhead and have an + * application that's secure and can be audited to verify that with much more ease than bolting + * security onto an application later. + * + * + * ## What trusted context types are supported? + * + * | Context | Notes | + * |---------------------|----------------| + * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered and the {@link ngSanitize $sanitize} module is present this will sanitize the value instead of throwing an error. | + * | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. | + * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`
Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | + * | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. | + * + * ## Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist}
+ * + * Each element in these arrays must be one of the following: + * + * - **'self'** + * - The special **string**, `'self'`, can be used to match against all URLs of the **same + * domain** as the application document using the **same protocol**. + * - **String** (except the special value `'self'`) + * - The string is matched against the full *normalized / absolute URL* of the resource + * being tested (substring matches are not good enough.) + * - There are exactly **two wildcard sequences** - `*` and `**`. All other characters + * match themselves. + * - `*`: matches zero or more occurrences of any character other than one of the following 6 + * characters: '`:`', '`/`', '`.`', '`?`', '`&`' and ';'. It's a useful wildcard for use + * in a whitelist. + * - `**`: matches zero or more occurrences of *any* character. As such, it's not + * not appropriate to use in for a scheme, domain, etc. as it would match too much. (e.g. + * http://**.example.com/ would match http://evil.com/?ignore=.example.com/ and that might + * not have been the intention.) Its usage at the very end of the path is ok. (e.g. + * http://foo.example.com/templates/**). + * - **RegExp** (*see caveat below*) + * - *Caveat*: While regular expressions are powerful and offer great flexibility, their syntax + * (and all the inevitable escaping) makes them *harder to maintain*. It's easy to + * accidentally introduce a bug when one updates a complex expression (imho, all regexes should + * have good test coverage.). For instance, the use of `.` in the regex is correct only in a + * small number of cases. A `.` character in the regex used when matching the scheme or a + * subdomain could be matched against a `:` or literal `.` that was likely not intended. It + * is highly recommended to use the string patterns and only fall back to regular expressions + * if they as a last resort. + * - The regular expression must be an instance of RegExp (i.e. not a string.) It is + * matched against the **entire** *normalized / absolute URL* of the resource being tested + * (even when the RegExp did not have the `^` and `$` codes.) In addition, any flags + * present on the RegExp (such as multiline, global, ignoreCase) are ignored. + * - If you are generating your JavaScript from some other templating engine (not + * recommended, e.g. in issue [#4006](https://github.com/angular/angular.js/issues/4006)), + * remember to escape your regular expression (and be aware that you might need more than + * one level of escaping depending on your templating engine and the way you interpolated + * the value.) Do make use of your platform's escaping mechanism as it might be good + * enough before coding your own. e.g. Ruby has + * [Regexp.escape(str)](http://www.ruby-doc.org/core-2.0.0/Regexp.html#method-c-escape) + * and Python has [re.escape](http://docs.python.org/library/re.html#re.escape). + * Javascript lacks a similar built in function for escaping. Take a look at Google + * Closure library's [goog.string.regExpEscape(s)]( + * http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962). + * + * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} for an example. + * + * ## Show me an example using SCE. + * + * + * + *
+ *

+ * User comments
+ * By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when + * $sanitize is available. If $sanitize isn't available, this results in an error instead of an + * exploit. + *
+ *
+ * {{userComment.name}}: + * + *
+ *
+ *
+ *
+ *
+ * + * + * var mySceApp = angular.module('mySceApp', ['ngSanitize']); + * + * mySceApp.controller("myAppController", function myAppController($http, $templateCache, $sce) { + * var self = this; + * $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) { + * self.userComments = userComments; + * }); + * self.explicitlyTrustedHtml = $sce.trustAsHtml( + * 'Hover over this text.'); + * }); + * + * + * + * [ + * { "name": "Alice", + * "htmlComment": + * "Is anyone reading this?" + * }, + * { "name": "Bob", + * "htmlComment": "Yes! Am I the only other one?" + * } + * ] + * + * + * + * describe('SCE doc demo', function() { + * it('should sanitize untrusted values', function() { + * expect(element.all(by.css('.htmlComment')).first().getInnerHtml()) + * .toBe('Is anyone reading this?'); + * }); + * + * it('should NOT sanitize explicitly trusted values', function() { + * expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe( + * 'Hover over this text.'); + * }); + * }); + * + *
+ * + * + * + * ## Can I disable SCE completely? + * + * Yes, you can. However, this is strongly discouraged. SCE gives you a lot of security benefits + * for little coding overhead. It will be much harder to take an SCE disabled application and + * either secure it on your own or enable SCE at a later stage. It might make sense to disable SCE + * for cases where you have a lot of existing code that was written before SCE was introduced and + * you're migrating them a module at a time. + * + * That said, here's how you can completely disable SCE: + * + * ``` + * angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) { + * // Completely disable SCE. For demonstration purposes only! + * // Do not use in new projects. + * $sceProvider.enabled(false); + * }); + * ``` * - * During testing this implementation is swapped with {@link ngMock.$httpBackend mock - * $httpBackend} which can be trained with responses. */ -function $HttpBackendProvider() { - this.$get = ['$browser', '$window', '$document', function($browser, $window, $document) { - return createHttpBackend($browser, XHR, $browser.defer, $window.angular.callbacks, - $document[0], $window.location.protocol.replace(':', '')); - }]; -} +/* jshint maxlen: 100 */ -function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument, locationProtocol) { - // TODO(vojta): fix the signature - return function(method, url, post, callback, headers, timeout, withCredentials) { - $browser.$$incOutstandingRequestCount(); - url = url || $browser.url(); +function $SceProvider() { + var enabled = true; - if (lowercase(method) == 'jsonp') { - var callbackId = '_' + (callbacks.counter++).toString(36); - callbacks[callbackId] = function(data) { - callbacks[callbackId].data = data; - }; + /** + * @ngdoc method + * @name $sceProvider#enabled + * @kind function + * + * @param {boolean=} value If provided, then enables/disables SCE. + * @return {boolean} true if SCE is enabled, false otherwise. + * + * @description + * Enables/disables SCE and returns the current value. + */ + this.enabled = function (value) { + if (arguments.length) { + enabled = !!value; + } + return enabled; + }; - jsonpReq(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId), - function() { - if (callbacks[callbackId].data) { - completeRequest(callback, 200, callbacks[callbackId].data); - } else { - completeRequest(callback, -2); - } - delete callbacks[callbackId]; - }); - } else { - var xhr = new XHR(); - xhr.open(method, url, true); - forEach(headers, function(value, key) { - if (value) xhr.setRequestHeader(key, value); - }); - var status; + /* Design notes on the default implementation for SCE. + * + * The API contract for the SCE delegate + * ------------------------------------- + * The SCE delegate object must provide the following 3 methods: + * + * - trustAs(contextEnum, value) + * This method is used to tell the SCE service that the provided value is OK to use in the + * contexts specified by contextEnum. It must return an object that will be accepted by + * getTrusted() for a compatible contextEnum and return this value. + * + * - valueOf(value) + * For values that were not produced by trustAs(), return them as is. For values that were + * produced by trustAs(), return the corresponding input value to trustAs. Basically, if + * trustAs is wrapping the given values into some type, this operation unwraps it when given + * such a value. + * + * - getTrusted(contextEnum, value) + * This function should return the a value that is safe to use in the context specified by + * contextEnum or throw and exception otherwise. + * + * NOTE: This contract deliberately does NOT state that values returned by trustAs() must be + * opaque or wrapped in some holder object. That happens to be an implementation detail. For + * instance, an implementation could maintain a registry of all trusted objects by context. In + * such a case, trustAs() would return the same object that was passed in. getTrusted() would + * return the same object passed in if it was found in the registry under a compatible context or + * throw an exception otherwise. An implementation might only wrap values some of the time based + * on some criteria. getTrusted() might return a value and not throw an exception for special + * constants or objects even if not wrapped. All such implementations fulfill this contract. + * + * + * A note on the inheritance model for SCE contexts + * ------------------------------------------------ + * I've used inheritance and made RESOURCE_URL wrapped types a subtype of URL wrapped types. This + * is purely an implementation details. + * + * The contract is simply this: + * + * getTrusted($sce.RESOURCE_URL, value) succeeding implies that getTrusted($sce.URL, value) + * will also succeed. + * + * Inheritance happens to capture this in a natural way. In some future, we + * may not use inheritance anymore. That is OK because no code outside of + * sce.js and sceSpecs.js would need to be aware of this detail. + */ - // In IE6 and 7, this might be called synchronously when xhr.send below is called and the - // response is in the cache. the promise api will ensure that to the app code the api is - // always async - xhr.onreadystatechange = function() { - if (xhr.readyState == 4) { - var responseHeaders = xhr.getAllResponseHeaders(); - - // TODO(vojta): remove once Firefox 21 gets released. - // begin: workaround to overcome Firefox CORS http response headers bug - // https://bugzilla.mozilla.org/show_bug.cgi?id=608735 - // Firefox already patched in nightly. Should land in Firefox 21. - - // CORS "simple response headers" http://www.w3.org/TR/cors/ - var value, - simpleHeaders = ["Cache-Control", "Content-Language", "Content-Type", - "Expires", "Last-Modified", "Pragma"]; - if (!responseHeaders) { - responseHeaders = ""; - forEach(simpleHeaders, function (header) { - var value = xhr.getResponseHeader(header); - if (value) { - responseHeaders += header + ": " + value + "\n"; - } - }); - } - // end of the workaround. + this.$get = ['$parse', '$sniffer', '$sceDelegate', function( + $parse, $sniffer, $sceDelegate) { + // Prereq: Ensure that we're not running in IE8 quirks mode. In that mode, IE allows + // the "expression(javascript expression)" syntax which is insecure. + if (enabled && $sniffer.msie && $sniffer.msieDocumentMode < 8) { + throw $sceMinErr('iequirks', + 'Strict Contextual Escaping does not support Internet Explorer version < 9 in quirks ' + + 'mode. You can fix this by adding the text to the top of your HTML ' + + 'document. See http://docs.angularjs.org/api/ng.$sce for more information.'); + } - completeRequest(callback, status || xhr.status, xhr.responseText, - responseHeaders); - } - }; + var sce = shallowCopy(SCE_CONTEXTS); - if (withCredentials) { - xhr.withCredentials = true; - } + /** + * @ngdoc method + * @name $sce#isEnabled + * @kind function + * + * @return {Boolean} true if SCE is enabled, false otherwise. If you want to set the value, you + * have to do it at module config time on {@link ng.$sceProvider $sceProvider}. + * + * @description + * Returns a boolean indicating if SCE is enabled. + */ + sce.isEnabled = function () { + return enabled; + }; + sce.trustAs = $sceDelegate.trustAs; + sce.getTrusted = $sceDelegate.getTrusted; + sce.valueOf = $sceDelegate.valueOf; - xhr.send(post || ''); + if (!enabled) { + sce.trustAs = sce.getTrusted = function(type, value) { return value; }; + sce.valueOf = identity; + } - if (timeout > 0) { - $browserDefer(function() { - status = -1; - xhr.abort(); - }, timeout); + /** + * @ngdoc method + * @name $sce#parseAs + * + * @description + * Converts Angular {@link guide/expression expression} into a function. This is like {@link + * ng.$parse $parse} and is identical when the expression is a literal constant. Otherwise, it + * wraps the expression in a call to {@link ng.$sce#getTrusted $sce.getTrusted(*type*, + * *result*)} + * + * @param {string} type The kind of SCE context in which this result will be used. + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + */ + sce.parseAs = function sceParseAs(type, expr) { + var parsed = $parse(expr); + if (parsed.literal && parsed.constant) { + return parsed; + } else { + return function sceParseAsTrusted(self, locals) { + return sce.getTrusted(type, parsed(self, locals)); + }; } - } + }; + /** + * @ngdoc method + * @name $sce#trustAs + * + * @description + * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such, + * returns an object that is trusted by angular for use in specified strict contextual + * escaping contexts (such as ng-bind-html, ng-include, any src attribute + * interpolation, any dom event binding attribute interpolation such as for onclick, etc.) + * that uses the provided value. See * {@link ng.$sce $sce} for enabling strict contextual + * escaping. + * + * @param {string} type The kind of context in which this value is safe for use. e.g. url, + * resource_url, html, js and css. + * @param {*} value The value that that should be considered trusted/safe. + * @returns {*} A value that can be used to stand in for the provided `value` in places + * where Angular expects a $sce.trustAs() return value. + */ - function completeRequest(callback, status, response, headersString) { - // URL_MATCH is defined in src/service/location.js - var protocol = (url.match(URL_MATCH) || ['', locationProtocol])[1]; + /** + * @ngdoc method + * @name $sce#trustAsHtml + * + * @description + * Shorthand method. `$sce.trustAsHtml(value)` → + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.HTML, value)`} + * + * @param {*} value The value to trustAs. + * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedHtml + * $sce.getTrustedHtml(value)} to obtain the original value. (privileged directives + * only accept expressions that are either literal constants or are the + * return value of {@link ng.$sce#trustAs $sce.trustAs}.) + */ - // fix status code for file protocol (it's always 0) - status = (protocol == 'file') ? (response ? 200 : 404) : status; + /** + * @ngdoc method + * @name $sce#trustAsUrl + * + * @description + * Shorthand method. `$sce.trustAsUrl(value)` → + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.URL, value)`} + * + * @param {*} value The value to trustAs. + * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedUrl + * $sce.getTrustedUrl(value)} to obtain the original value. (privileged directives + * only accept expressions that are either literal constants or are the + * return value of {@link ng.$sce#trustAs $sce.trustAs}.) + */ - // normalize IE bug (http://bugs.jquery.com/ticket/1450) - status = status == 1223 ? 204 : status; + /** + * @ngdoc method + * @name $sce#trustAsResourceUrl + * + * @description + * Shorthand method. `$sce.trustAsResourceUrl(value)` → + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.RESOURCE_URL, value)`} + * + * @param {*} value The value to trustAs. + * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedResourceUrl + * $sce.getTrustedResourceUrl(value)} to obtain the original value. (privileged directives + * only accept expressions that are either literal constants or are the return + * value of {@link ng.$sce#trustAs $sce.trustAs}.) + */ - callback(status, response, headersString); - $browser.$$completeOutstandingRequest(noop); - } - }; + /** + * @ngdoc method + * @name $sce#trustAsJs + * + * @description + * Shorthand method. `$sce.trustAsJs(value)` → + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.JS, value)`} + * + * @param {*} value The value to trustAs. + * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedJs + * $sce.getTrustedJs(value)} to obtain the original value. (privileged directives + * only accept expressions that are either literal constants or are the + * return value of {@link ng.$sce#trustAs $sce.trustAs}.) + */ - function jsonpReq(url, done) { - // we can't use jQuery/jqLite here because jQuery does crazy shit with script elements, e.g.: - // - fetches local scripts via XHR and evals them - // - adds and immediately removes script elements from the document - var script = rawDocument.createElement('script'), - doneWrapper = function() { - rawDocument.body.removeChild(script); - if (done) done(); - }; + /** + * @ngdoc method + * @name $sce#getTrusted + * + * @description + * Delegates to {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted`}. As such, + * takes the result of a {@link ng.$sce#trustAs `$sce.trustAs`}() call and returns the + * originally supplied value if the queried context type is a supertype of the created type. + * If this condition isn't satisfied, throws an exception. + * + * @param {string} type The kind of context in which this value is to be used. + * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs `$sce.trustAs`} + * call. + * @returns {*} The value the was originally provided to + * {@link ng.$sce#trustAs `$sce.trustAs`} if valid in this context. + * Otherwise, throws an exception. + */ - script.type = 'text/javascript'; - script.src = url; + /** + * @ngdoc method + * @name $sce#getTrustedHtml + * + * @description + * Shorthand method. `$sce.getTrustedHtml(value)` → + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.HTML, value)`} + * + * @param {*} value The value to pass to `$sce.getTrusted`. + * @returns {*} The return value of `$sce.getTrusted($sce.HTML, value)` + */ - if (msie) { - script.onreadystatechange = function() { - if (/loaded|complete/.test(script.readyState)) doneWrapper(); - }; - } else { - script.onload = script.onerror = doneWrapper; - } + /** + * @ngdoc method + * @name $sce#getTrustedCss + * + * @description + * Shorthand method. `$sce.getTrustedCss(value)` → + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.CSS, value)`} + * + * @param {*} value The value to pass to `$sce.getTrusted`. + * @returns {*} The return value of `$sce.getTrusted($sce.CSS, value)` + */ - rawDocument.body.appendChild(script); - } -} + /** + * @ngdoc method + * @name $sce#getTrustedUrl + * + * @description + * Shorthand method. `$sce.getTrustedUrl(value)` → + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.URL, value)`} + * + * @param {*} value The value to pass to `$sce.getTrusted`. + * @returns {*} The return value of `$sce.getTrusted($sce.URL, value)` + */ -/** - * @ngdoc object - * @name ng.$locale - * - * @description - * $locale service provides localization rules for various Angular components. As of right now the - * only public api is: - * - * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`) - */ -function $LocaleProvider(){ - this.$get = function() { - return { - id: 'en-us', + /** + * @ngdoc method + * @name $sce#getTrustedResourceUrl + * + * @description + * Shorthand method. `$sce.getTrustedResourceUrl(value)` → + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.RESOURCE_URL, value)`} + * + * @param {*} value The value to pass to `$sceDelegate.getTrusted`. + * @returns {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)` + */ - NUMBER_FORMATS: { - DECIMAL_SEP: '.', - GROUP_SEP: ',', - PATTERNS: [ - { // Decimal Pattern - minInt: 1, - minFrac: 0, - maxFrac: 3, - posPre: '', - posSuf: '', - negPre: '-', - negSuf: '', - gSize: 3, - lgSize: 3 - },{ //Currency Pattern - minInt: 1, - minFrac: 2, - maxFrac: 2, - posPre: '\u00A4', - posSuf: '', - negPre: '(\u00A4', - negSuf: ')', - gSize: 3, - lgSize: 3 - } - ], - CURRENCY_SYM: '$' - }, + /** + * @ngdoc method + * @name $sce#getTrustedJs + * + * @description + * Shorthand method. `$sce.getTrustedJs(value)` → + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.JS, value)`} + * + * @param {*} value The value to pass to `$sce.getTrusted`. + * @returns {*} The return value of `$sce.getTrusted($sce.JS, value)` + */ - DATETIME_FORMATS: { - MONTH: 'January,February,March,April,May,June,July,August,September,October,November,December' - .split(','), - SHORTMONTH: 'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(','), - DAY: 'Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday'.split(','), - SHORTDAY: 'Sun,Mon,Tue,Wed,Thu,Fri,Sat'.split(','), - AMPMS: ['AM','PM'], - medium: 'MMM d, y h:mm:ss a', - short: 'M/d/yy h:mm a', - fullDate: 'EEEE, MMMM d, y', - longDate: 'MMMM d, y', - mediumDate: 'MMM d, y', - shortDate: 'M/d/yy', - mediumTime: 'h:mm:ss a', - shortTime: 'h:mm a' - }, + /** + * @ngdoc method + * @name $sce#parseAsHtml + * + * @description + * Shorthand method. `$sce.parseAsHtml(expression string)` → + * {@link ng.$sce#parse `$sce.parseAs($sce.HTML, value)`} + * + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + */ + + /** + * @ngdoc method + * @name $sce#parseAsCss + * + * @description + * Shorthand method. `$sce.parseAsCss(value)` → + * {@link ng.$sce#parse `$sce.parseAs($sce.CSS, value)`} + * + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + */ + + /** + * @ngdoc method + * @name $sce#parseAsUrl + * + * @description + * Shorthand method. `$sce.parseAsUrl(value)` → + * {@link ng.$sce#parse `$sce.parseAs($sce.URL, value)`} + * + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + */ + + /** + * @ngdoc method + * @name $sce#parseAsResourceUrl + * + * @description + * Shorthand method. `$sce.parseAsResourceUrl(value)` → + * {@link ng.$sce#parse `$sce.parseAs($sce.RESOURCE_URL, value)`} + * + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + */ + + /** + * @ngdoc method + * @name $sce#parseAsJs + * + * @description + * Shorthand method. `$sce.parseAsJs(value)` → + * {@link ng.$sce#parse `$sce.parseAs($sce.JS, value)`} + * + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + */ + + // Shorthand delegations. + var parse = sce.parseAs, + getTrusted = sce.getTrusted, + trustAs = sce.trustAs; + + forEach(SCE_CONTEXTS, function (enumValue, name) { + var lName = lowercase(name); + sce[camelCase("parse_as_" + lName)] = function (expr) { + return parse(enumValue, expr); + }; + sce[camelCase("get_trusted_" + lName)] = function (value) { + return getTrusted(enumValue, value); + }; + sce[camelCase("trust_as_" + lName)] = function (value) { + return trustAs(enumValue, value); + }; + }); - pluralCat: function(num) { - if (num === 1) { - return 'one'; + return sce; + }]; +} + +/** + * !!! This is an undocumented "private" service !!! + * + * @name $sniffer + * @requires $window + * @requires $document + * + * @property {boolean} history Does the browser support html5 history api ? + * @property {boolean} hashchange Does the browser support hashchange event ? + * @property {boolean} transitions Does the browser support CSS transition events ? + * @property {boolean} animations Does the browser support CSS animation events ? + * + * @description + * This is very simple implementation of testing browser's features. + */ +function $SnifferProvider() { + this.$get = ['$window', '$document', function($window, $document) { + var eventSupport = {}, + android = + int((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), + boxee = /Boxee/i.test(($window.navigator || {}).userAgent), + document = $document[0] || {}, + documentMode = document.documentMode, + vendorPrefix, + vendorRegex = /^(Moz|webkit|O|ms)(?=[A-Z])/, + bodyStyle = document.body && document.body.style, + transitions = false, + animations = false, + match; + + if (bodyStyle) { + for(var prop in bodyStyle) { + if(match = vendorRegex.exec(prop)) { + vendorPrefix = match[0]; + vendorPrefix = vendorPrefix.substr(0, 1).toUpperCase() + vendorPrefix.substr(1); + break; } - return 'other'; } + + if(!vendorPrefix) { + vendorPrefix = ('WebkitOpacity' in bodyStyle) && 'webkit'; + } + + transitions = !!(('transition' in bodyStyle) || (vendorPrefix + 'Transition' in bodyStyle)); + animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle)); + + if (android && (!transitions||!animations)) { + transitions = isString(document.body.style.webkitTransition); + animations = isString(document.body.style.webkitAnimation); + } + } + + + return { + // Android has history.pushState, but it does not update location correctly + // so let's not use the history API at all. + // http://code.google.com/p/android/issues/detail?id=17471 + // https://github.com/angular/angular.js/issues/904 + + // older webkit browser (533.9) on Boxee box has exactly the same problem as Android has + // so let's not use the history API also + // We are purposefully using `!(android < 4)` to cover the case when `android` is undefined + // jshint -W018 + history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee), + // jshint +W018 + hashchange: 'onhashchange' in $window && + // IE8 compatible mode lies + (!documentMode || documentMode > 7), + hasEvent: function(event) { + // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have + // it. In particular the event is not fired when backspace or delete key are pressed or + // when cut operation is performed. + if (event == 'input' && msie == 9) return false; + + if (isUndefined(eventSupport[event])) { + var divElm = document.createElement('div'); + eventSupport[event] = 'on' + event in divElm; + } + + return eventSupport[event]; + }, + csp: csp(), + vendorPrefix: vendorPrefix, + transitions : transitions, + animations : animations, + android: android, + msie : msie, + msieDocumentMode: documentMode }; - }; + }]; } function $TimeoutProvider() { @@ -9438,9 +14359,8 @@ function $TimeoutProvider() { /** - * @ngdoc function - * @name ng.$timeout - * @requires $browser + * @ngdoc service + * @name $timeout * * @description * Angular's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch @@ -9461,12 +14381,13 @@ function $TimeoutProvider() { * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. * @returns {Promise} Promise that will be resolved when the timeout is reached. The value this * promise will be resolved with is the return value of the `fn` function. + * */ function timeout(fn, delay, invokeApply) { var deferred = $q.defer(), promise = deferred.promise, skipApply = (isDefined(invokeApply) && !invokeApply), - timeoutId, cleanup; + timeoutId; timeoutId = $browser.defer(function() { try { @@ -9475,26 +14396,23 @@ function $TimeoutProvider() { deferred.reject(e); $exceptionHandler(e); } + finally { + delete deferreds[promise.$$timeoutId]; + } if (!skipApply) $rootScope.$apply(); }, delay); - cleanup = function() { - delete deferreds[promise.$$timeoutId]; - }; - promise.$$timeoutId = timeoutId; deferreds[timeoutId] = deferred; - promise.then(cleanup, cleanup); return promise; } /** - * @ngdoc function - * @name ng.$timeout#cancel - * @methodOf ng.$timeout + * @ngdoc method + * @name $timeout#cancel * * @description * Cancels a task associated with the `promise`. As a result of this, the promise will be @@ -9507,6 +14425,7 @@ function $TimeoutProvider() { timeout.cancel = function(promise) { if (promise && promise.$$timeoutId in deferreds) { deferreds[promise.$$timeoutId].reject('canceled'); + delete deferreds[promise.$$timeoutId]; return $browser.defer.cancel(promise.$$timeoutId); } return false; @@ -9516,16 +14435,175 @@ function $TimeoutProvider() { }]; } +// NOTE: The usage of window and document instead of $window and $document here is +// deliberate. This service depends on the specific behavior of anchor nodes created by the +// browser (resolving and parsing URLs) that is unlikely to be provided by mock objects and +// cause us to break tests. In addition, when the browser resolves a URL for XHR, it +// doesn't know about mocked locations and resolves URLs to the real document - which is +// exactly the behavior needed here. There is little value is mocking these out for this +// service. +var urlParsingNode = document.createElement("a"); +var originUrl = urlResolve(window.location.href, true); + + /** - * @ngdoc object - * @name ng.$filterProvider + * + * Implementation Notes for non-IE browsers + * ---------------------------------------- + * Assigning a URL to the href property of an anchor DOM node, even one attached to the DOM, + * results both in the normalizing and parsing of the URL. Normalizing means that a relative + * URL will be resolved into an absolute URL in the context of the application document. + * Parsing means that the anchor node's host, hostname, protocol, port, pathname and related + * properties are all populated to reflect the normalized URL. This approach has wide + * compatibility - Safari 1+, Mozilla 1+, Opera 7+,e etc. See + * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html + * + * Implementation Notes for IE + * --------------------------- + * IE >= 8 and <= 10 normalizes the URL when assigned to the anchor node similar to the other + * browsers. However, the parsed components will not be set if the URL assigned did not specify + * them. (e.g. if you assign a.href = "foo", then a.protocol, a.host, etc. will be empty.) We + * work around that by performing the parsing in a 2nd step by taking a previously normalized + * URL (e.g. by assigning to a.href) and assigning it a.href again. This correctly populates the + * properties such as protocol, hostname, port, etc. + * + * IE7 does not normalize the URL when assigned to an anchor node. (Apparently, it does, if one + * uses the inner HTML approach to assign the URL as part of an HTML snippet - + * http://stackoverflow.com/a/472729) However, setting img[src] does normalize the URL. + * Unfortunately, setting img[src] to something like "javascript:foo" on IE throws an exception. + * Since the primary usage for normalizing URLs is to sanitize such URLs, we can't use that + * method and IE < 8 is unsupported. + * + * References: + * http://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement + * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html + * http://url.spec.whatwg.org/#urlutils + * https://github.com/angular/angular.js/pull/2902 + * http://james.padolsey.com/javascript/parsing-urls-with-the-dom/ + * + * @kind function + * @param {string} url The URL to be parsed. + * @description Normalizes and parses a URL. + * @returns {object} Returns the normalized URL as a dictionary. + * + * | member name | Description | + * |---------------|----------------| + * | href | A normalized version of the provided URL if it was not an absolute URL | + * | protocol | The protocol including the trailing colon | + * | host | The host and port (if the port is non-default) of the normalizedUrl | + * | search | The search params, minus the question mark | + * | hash | The hash string, minus the hash symbol + * | hostname | The hostname + * | port | The port, without ":" + * | pathname | The pathname, beginning with "/" + * + */ +function urlResolve(url, base) { + var href = url; + + if (msie) { + // Normalize before parse. Refer Implementation Notes on why this is + // done in two steps on IE. + urlParsingNode.setAttribute("href", href); + href = urlParsingNode.href; + } + + urlParsingNode.setAttribute('href', href); + + // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils + return { + href: urlParsingNode.href, + protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '', + host: urlParsingNode.host, + search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '', + hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '', + hostname: urlParsingNode.hostname, + port: urlParsingNode.port, + pathname: (urlParsingNode.pathname.charAt(0) === '/') + ? urlParsingNode.pathname + : '/' + urlParsingNode.pathname + }; +} + +/** + * Parse a request URL and determine whether this is a same-origin request as the application document. + * + * @param {string|object} requestUrl The url of the request as a string that will be resolved + * or a parsed URL object. + * @returns {boolean} Whether the request is for the same origin as the application document. + */ +function urlIsSameOrigin(requestUrl) { + var parsed = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl; + return (parsed.protocol === originUrl.protocol && + parsed.host === originUrl.host); +} + +/** + * @ngdoc service + * @name $window + * * @description + * A reference to the browser's `window` object. While `window` + * is globally available in JavaScript, it causes testability problems, because + * it is a global variable. In angular we always refer to it through the + * `$window` service, so it may be overridden, removed or mocked for testing. * - * Filters are just functions which transform input to an output. However filters need to be Dependency Injected. To - * achieve this a filter definition consists of a factory function which is annotated with dependencies and is - * responsible for creating a filter function. + * Expressions, like the one defined for the `ngClick` directive in the example + * below, are evaluated with respect to the current scope. Therefore, there is + * no risk of inadvertently coding in a dependency on a global value in such an + * expression. * - *
+ * @example
+   
+     
+       
+       
+ + +
+
+ + it('should display the greeting in the input box', function() { + element(by.model('greeting')).sendKeys('Hello, E2E Tests'); + // If we click the button it will block the test runner + // element(':button').click(); + }); + +
+ */ +function $WindowProvider(){ + this.$get = valueFn(window); +} + +/* global currencyFilter: true, + dateFilter: true, + filterFilter: true, + jsonFilter: true, + limitToFilter: true, + lowercaseFilter: true, + numberFilter: true, + orderByFilter: true, + uppercaseFilter: true, + */ + +/** + * @ngdoc provider + * @name $filterProvider + * @description + * + * Filters are just functions which transform input to an output. However filters need to be + * Dependency Injected. To achieve this a filter definition consists of a factory function which is + * annotated with dependencies and is responsible for creating a filter function. + * + * ```js * // Filter registration * function MyModule($provide, $filterProvider) { * // create a service to demonstrate injection (not always needed) @@ -9544,10 +14622,12 @@ function $TimeoutProvider() { * }; * }); * } - *
+ * ``` * - * The filter function is registered with the `$injector` under the filter name suffixe with `Filter`. - *
+ * The filter function is registered with the `$injector` under the filter name suffix with
+ * `Filter`.
+ *
+ * ```js
  *   it('should be the same instance', inject(
  *     function($filterProvider) {
  *       $filterProvider.register('reverse', function(){
@@ -9557,29 +14637,17 @@ function $TimeoutProvider() {
  *     function($filter, reverseFilter) {
  *       expect($filter('reverse')).toBe(reverseFilter);
  *     });
- * 
+ * ``` * * * For more information about how angular filters work, and how to create your own filters, see - * {@link guide/dev_guide.templates.filters Understanding Angular Filters} in the angular Developer - * Guide. - */ -/** - * @ngdoc method - * @name ng.$filterProvider#register - * @methodOf ng.$filterProvider - * @description - * Register filter factory function. - * - * @param {String} name Name of the filter. - * @param {function} fn The filter factory function which is injectable. + * {@link guide/filter Filters} in the Angular Developer Guide. */ - /** - * @ngdoc function - * @name ng.$filter - * @function + * @ngdoc service + * @name $filter + * @kind function * @description * Filters are used for formatting data displayed to the user. * @@ -9589,24 +14657,69 @@ function $TimeoutProvider() { * * @param {String} name Name of the filter function to retrieve * @return {Function} the filter function - */ + * @example + + +
+

{{ originalText }}

+

{{ filteredText }}

+
+
+ + + angular.module('filterExample', []) + .controller('MainCtrl', function($scope, $filter) { + $scope.originalText = 'hello'; + $scope.filteredText = $filter('uppercase')($scope.originalText); + }); + +
+ */ $FilterProvider.$inject = ['$provide']; function $FilterProvider($provide) { var suffix = 'Filter'; + /** + * @ngdoc method + * @name $filterProvider#register + * @param {string|Object} name Name of the filter function, or an object map of filters where + * the keys are the filter names and the values are the filter factories. + * @returns {Object} Registered filter instance, or if a map of filters was provided then a map + * of the registered filter instances. + */ function register(name, factory) { - return $provide.factory(name + suffix, factory); + if(isObject(name)) { + var filters = {}; + forEach(name, function(filter, key) { + filters[key] = register(key, filter); + }); + return filters; + } else { + return $provide.factory(name + suffix, factory); + } } this.register = register; this.$get = ['$injector', function($injector) { return function(name) { return $injector.get(name + suffix); - } + }; }]; //////////////////////////////////////// + /* global + currencyFilter: false, + dateFilter: false, + filterFilter: false, + jsonFilter: false, + limitToFilter: false, + lowercaseFilter: false, + numberFilter: false, + orderByFilter: false, + uppercaseFilter: false, + */ + register('currency', currencyFilter); register('date', dateFilter); register('filter', filterFilter); @@ -9620,23 +14733,20 @@ function $FilterProvider($provide) { /** * @ngdoc filter - * @name ng.filter:filter - * @function + * @name filter + * @kind function * * @description * Selects a subset of items from `array` and returns it as a new array. * - * Note: This function is used to augment the `Array` type in Angular expressions. See - * {@link ng.$filter} for more information about Angular arrays. - * * @param {Array} array The source array. * @param {string|Object|function()} expression The predicate to be used for selecting items from * `array`. * * Can be one of: * - * - `string`: Predicate that results in a substring match using the value of `expression` - * string. All strings or objects with string properties in `array` that contain this string + * - `string`: The string is evaluated as an expression and the resulting value is used for substring match against + * the contents of the `array`. All strings or objects with string properties in `array` that contain this string * will be returned. The predicate can be negated by prefixing the string with `!`. * * - `Object`: A pattern object can be used to filter specific properties on objects contained @@ -9644,20 +14754,39 @@ function $FilterProvider($provide) { * which have property `name` containing "M" and property `phone` containing "1". A special * property name `$` can be used (as in `{$:"text"}`) to accept a match against any * property of the object. That's equivalent to the simple substring match with a `string` - * as described above. + * as described above. The predicate can be negated by prefixing the string with `!`. + * For Example `{name: "!M"}` predicate will return an array of items which have property `name` + * not containing "M". * - * - `function`: A predicate function can be used to write arbitrary filters. The function is + * - `function(value)`: A predicate function can be used to write arbitrary filters. The function is * called for each element of `array`. The final result is an array of those elements that * the predicate returned true for. * + * @param {function(actual, expected)|true|undefined} comparator Comparator which is used in + * determining if the expected value (from the filter expression) and actual value (from + * the object in the array) should be considered a match. + * + * Can be one of: + * + * - `function(actual, expected)`: + * The function will be given the object value and the predicate value to compare and + * should return true if the item should be included in filtered result. + * + * - `true`: A shorthand for `function(actual, expected) { return angular.equals(expected, actual)}`. + * this is essentially strict comparison of expected and actual. + * + * - `false|undefined`: A short hand for a function which will look for a substring match in case + * insensitive way. + * * @example - - + +
+ {name:'Julie', phone:'555-8765'}, + {name:'Juliette', phone:'555-5678'}]"> Search: @@ -9671,37 +14800,59 @@ function $FilterProvider($provide) { Any:
Name only
Phone only
+ Equality
- - - + + +
NamePhone
{{friend.name}}{{friend.phone}}
{{friendObj.name}}{{friendObj.phone}}
-
- - it('should search across all fields when filtering with a string', function() { - input('searchText').enter('m'); - expect(repeater('#searchTextResults tr', 'friend in friends').column('friend.name')). - toEqual(['Mary', 'Mike', 'Adam']); + + + var expectFriendNames = function(expectedNames, key) { + element.all(by.repeater(key + ' in friends').column(key + '.name')).then(function(arr) { + arr.forEach(function(wd, i) { + expect(wd.getText()).toMatch(expectedNames[i]); + }); + }); + }; - input('searchText').enter('76'); - expect(repeater('#searchTextResults tr', 'friend in friends').column('friend.name')). - toEqual(['John', 'Julie']); + it('should search across all fields when filtering with a string', function() { + var searchText = element(by.model('searchText')); + searchText.clear(); + searchText.sendKeys('m'); + expectFriendNames(['Mary', 'Mike', 'Adam'], 'friend'); + + searchText.clear(); + searchText.sendKeys('76'); + expectFriendNames(['John', 'Julie'], 'friend'); }); it('should search in specific fields when filtering with a predicate object', function() { - input('search.$').enter('i'); - expect(repeater('#searchObjResults tr', 'friend in friends').column('friend.name')). - toEqual(['Mary', 'Mike', 'Julie']); + var searchAny = element(by.model('search.$')); + searchAny.clear(); + searchAny.sendKeys('i'); + expectFriendNames(['Mary', 'Mike', 'Julie', 'Juliette'], 'friendObj'); }); - -
+ it('should use a equal comparison when comparator is true', function() { + var searchName = element(by.model('search.name')); + var strict = element(by.model('strict')); + searchName.clear(); + searchName.sendKeys('Julie'); + strict.click(); + expectFriendNames(['Julie'], 'friendObj'); + }); + + */ function filterFilter() { - return function(array, expression) { + return function(array, expression, comparator) { if (!isArray(array)) return array; - var predicates = []; + + var comparatorType = typeof(comparator), + predicates = []; + predicates.check = function(value) { for (var j = 0; j < predicates.length; j++) { if(!predicates[j](value)) { @@ -9710,23 +14861,52 @@ function filterFilter() { } return true; }; + + if (comparatorType !== 'function') { + if (comparatorType === 'boolean' && comparator) { + comparator = function(obj, text) { + return angular.equals(obj, text); + }; + } else { + comparator = function(obj, text) { + if (obj && text && typeof obj === 'object' && typeof text === 'object') { + for (var objKey in obj) { + if (objKey.charAt(0) !== '$' && hasOwnProperty.call(obj, objKey) && + comparator(obj[objKey], text[objKey])) { + return true; + } + } + return false; + } + text = (''+text).toLowerCase(); + return (''+obj).toLowerCase().indexOf(text) > -1; + }; + } + } + var search = function(obj, text){ - if (text.charAt(0) === '!') { + if (typeof text === 'string' && text.charAt(0) === '!') { return !search(obj, text.substr(1)); } switch (typeof obj) { - case "boolean": - case "number": - case "string": - return ('' + obj).toLowerCase().indexOf(text) > -1; - case "object": - for ( var objKey in obj) { - if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) { - return true; - } + case 'boolean': + case 'number': + case 'string': + return comparator(obj, text); + case 'object': + switch (typeof text) { + case 'object': + return comparator(obj, text); + default: + for ( var objKey in obj) { + if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) { + return true; + } + } + break; } return false; - case "array": + case 'array': for ( var i = 0; i < obj.length; i++) { if (search(obj[i], text)) { return true; @@ -9738,30 +14918,21 @@ function filterFilter() { } }; switch (typeof expression) { - case "boolean": - case "number": - case "string": + case 'boolean': + case 'number': + case 'string': + // Set up expression object and fall through expression = {$:expression}; - case "object": + // jshint -W086 + case 'object': + // jshint +W086 for (var key in expression) { - if (key == '$') { - (function() { - var text = (''+expression[key]).toLowerCase(); - if (!text) return; - predicates.push(function(value) { - return search(value, text); - }); - })(); - } else { - (function() { - var path = key; - var text = (''+expression[key]).toLowerCase(); - if (!text) return; - predicates.push(function(value) { - return search(getter(value, path), text); - }); - })(); - } + (function(path) { + if (typeof expression[path] === 'undefined') return; + predicates.push(function(value) { + return search(path == '$' ? value : (value && value[path]), expression[path]); + }); + })(key); } break; case 'function': @@ -9778,13 +14949,13 @@ function filterFilter() { } } return filtered; - } + }; } /** * @ngdoc filter - * @name ng.filter:currency - * @function + * @name currency + * @kind function * * @description * Formats a number as a currency (ie $1,234.56). When no currency symbol is provided, default @@ -9796,31 +14967,38 @@ function filterFilter() { * * * @example - - + + -
+

- default currency symbol ($): {{amount | currency}}
- custom currency identifier (USD$): {{amount | currency:"USD$"}} + default currency symbol ($): {{amount | currency}}
+ custom currency identifier (USD$): {{amount | currency:"USD$"}}
- - + + it('should init with 1234.56', function() { - expect(binding('amount | currency')).toBe('$1,234.56'); - expect(binding('amount | currency:"USD$"')).toBe('USD$1,234.56'); + expect(element(by.id('currency-default')).getText()).toBe('$1,234.56'); + expect(element(by.binding('amount | currency:"USD$"')).getText()).toBe('USD$1,234.56'); }); it('should update', function() { - input('amount').enter('-1234'); - expect(binding('amount | currency')).toBe('($1,234.00)'); - expect(binding('amount | currency:"USD$"')).toBe('(USD$1,234.00)'); + if (browser.params.browser == 'safari') { + // Safari does not understand the minus key. See + // https://github.com/angular/protractor/issues/481 + return; + } + element(by.model('amount')).clear(); + element(by.model('amount')).sendKeys('-1234'); + expect(element(by.id('currency-default')).getText()).toBe('($1,234.00)'); + expect(element(by.binding('amount | currency:"USD$"')).getText()).toBe('(USD$1,234.00)'); }); - - + + */ currencyFilter.$inject = ['$locale']; function currencyFilter($locale) { @@ -9834,8 +15012,8 @@ function currencyFilter($locale) { /** * @ngdoc filter - * @name ng.filter:number - * @function + * @name number + * @kind function * * @description * Formats a number as text. @@ -9843,39 +15021,43 @@ function currencyFilter($locale) { * If the input is not a number an empty string is returned. * * @param {number|string} number Number to format. - * @param {(number|string)=} [fractionSize=2] Number of decimal places to round the number to. + * @param {(number|string)=} fractionSize Number of decimal places to round the number to. + * If this is not provided then the fraction size is computed from the current locale's number + * formatting pattern. In the case of the default locale, it will be 3. * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit. * * @example - - + + -
+
Enter number:
- Default formatting: {{val | number}}
- No fractions: {{val | number:0}}
- Negative number: {{-val | number:4}} + Default formatting: {{val | number}}
+ No fractions: {{val | number:0}}
+ Negative number: {{-val | number:4}}
- - + + it('should format numbers', function() { - expect(binding('val | number')).toBe('1,234.568'); - expect(binding('val | number:0')).toBe('1,235'); - expect(binding('-val | number:4')).toBe('-1,234.5679'); + expect(element(by.id('number-default')).getText()).toBe('1,234.568'); + expect(element(by.binding('val | number:0')).getText()).toBe('1,235'); + expect(element(by.binding('-val | number:4')).getText()).toBe('-1,234.5679'); }); it('should update', function() { - input('val').enter('3374.333'); - expect(binding('val | number')).toBe('3,374.333'); - expect(binding('val | number:0')).toBe('3,374'); - expect(binding('-val | number:4')).toBe('-3,374.3330'); - }); - - + element(by.model('val')).clear(); + element(by.model('val')).sendKeys('3374.333'); + expect(element(by.id('number-default')).getText()).toBe('3,374.333'); + expect(element(by.binding('val | number:0')).getText()).toBe('3,374'); + expect(element(by.binding('-val | number:4')).getText()).toBe('-3,374.3330'); + }); + + */ @@ -9890,7 +15072,7 @@ function numberFilter($locale) { var DECIMAL_SEP = '.'; function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { - if (isNaN(number) || !isFinite(number)) return ''; + if (number == null || !isFinite(number) || isObject(number)) return ''; var isNegative = number < 0; number = Math.abs(number); @@ -9903,6 +15085,7 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { var match = numStr.match(/([\d\.]+)e(-?)(\d+)/); if (match && match[2] == '-' && match[3] > fractionSize + 1) { numStr = '0'; + number = 0; } else { formatedText = numStr; hasExponent = true; @@ -9917,19 +15100,26 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac); } - var pow = Math.pow(10, fractionSize); - number = Math.round(number * pow) / pow; + // safely round numbers in JS without hitting imprecisions of floating-point arithmetics + // inspired by: + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round + number = +(Math.round(+(number.toString() + 'e' + fractionSize)).toString() + 'e' + -fractionSize); + + if (number === 0) { + isNegative = false; + } + var fraction = ('' + number).split(DECIMAL_SEP); var whole = fraction[0]; fraction = fraction[1] || ''; - var pos = 0, + var i, pos = 0, lgroup = pattern.lgSize, group = pattern.gSize; if (whole.length >= (lgroup + group)) { pos = whole.length - lgroup; - for (var i = 0; i < pos; i++) { + for (i = 0; i < pos; i++) { if ((pos - i)%group === 0 && i !== 0) { formatedText += groupSep; } @@ -9950,6 +15140,11 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { } if (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize); + } else { + + if (fractionSize > 0 && number > -1 && number < 1) { + formatedText = number.toFixed(fractionSize); + } } parts.push(isNegative ? pattern.negPre : pattern.posPre); @@ -10024,6 +15219,9 @@ var DATE_FORMATS = { m: dateGetter('Minutes', 1), ss: dateGetter('Seconds', 2), s: dateGetter('Seconds', 1), + // while ISO 8601 requires fractions to be prefixed with `.` or `,` + // we can be just safely rely on using `sss` since we currently don't support single or two digit fractions + sss: dateGetter('Milliseconds', 3), EEEE: dateStrGetter('Day'), EEE: dateStrGetter('Day', true), a: ampmGetter, @@ -10031,12 +15229,12 @@ var DATE_FORMATS = { }; var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/, - NUMBER_STRING = /^\d+$/; + NUMBER_STRING = /^\-?\d+$/; /** * @ngdoc filter - * @name ng.filter:date - * @function + * @name date + * @kind function * * @description * Formats `date` to a string based on the requested `format`. @@ -10062,6 +15260,7 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+ * * `'m'`: Minute in hour (0-59) * * `'ss'`: Second in minute, padded (00-59) * * `'s'`: Second in minute (0-59) + * * `'.sss' or ',sss'`: Millisecond in second, padded (000-999) * * `'a'`: am/pm marker * * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-+1200) * @@ -10073,18 +15272,18 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+ * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 pm) * * `'fullDate'`: equivalent to `'EEEE, MMMM d,y'` for en_US locale * (e.g. Friday, September 3, 2010) - * * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010 + * * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010) * * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US locale (e.g. Sep 3, 2010) * * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10) * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 pm) * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 pm) * - * `format` string can contain literal values. These need to be quoted with single quotes (e.g. - * `"h 'in the morning'"`). In order to output single quote, use two single quotes in a sequence - * (e.g. `"h o''clock"`). + * `format` string can contain literal values. These need to be escaped by surrounding with single quotes (e.g. + * `"h 'in the morning'"`). In order to output a single quote, escape it - i.e., two single quotes in a sequence + * (e.g. `"h 'o''clock'"`). * * @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or - * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.SSSZ and its + * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.sssZ and its * shorter versions like yyyy-MM-ddTHH:mmZ, yyyy-MM-dd or yyyyMMddTHHmmssZ). If no timezone is * specified in the string input, the time is considered to be in the local timezone. * @param {string=} format Formatting rules (see Description). If not specified, @@ -10092,44 +15291,56 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+ * @returns {string} Formatted string or the input if input is not recognized as date/millis. * * @example - - + + {{1288323623006 | date:'medium'}}: - {{1288323623006 | date:'medium'}}
+ {{1288323623006 | date:'medium'}}
{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}: - {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
+ {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
{{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}: - {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
-
- + {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
+ {{1288323623006 | date:"MM/dd/yyyy 'at' h:mma"}}: + {{'1288323623006' | date:"MM/dd/yyyy 'at' h:mma"}}
+ + it('should format date', function() { - expect(binding("1288323623006 | date:'medium'")). + expect(element(by.binding("1288323623006 | date:'medium'")).getText()). toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/); - expect(binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")). + expect(element(by.binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).getText()). toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} (\-|\+)?\d{4}/); - expect(binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")). + expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()). toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/); + expect(element(by.binding("'1288323623006' | date:\"MM/dd/yyyy 'at' h:mma\"")).getText()). + toMatch(/10\/2\d\/2010 at \d{1,2}:\d{2}(AM|PM)/); }); -
-
+ + */ dateFilter.$inject = ['$locale']; function dateFilter($locale) { var R_ISO8601_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/; - function jsonStringToDate(string){ + // 1 2 3 4 5 6 7 8 9 10 11 + function jsonStringToDate(string) { var match; if (match = string.match(R_ISO8601_STR)) { var date = new Date(0), tzHour = 0, - tzMin = 0; + tzMin = 0, + dateSetter = match[8] ? date.setUTCFullYear : date.setFullYear, + timeSetter = match[8] ? date.setUTCHours : date.setHours; + if (match[9]) { tzHour = int(match[9] + match[10]); tzMin = int(match[9] + match[11]); } - date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3])); - date.setUTCHours(int(match[4]||0) - tzHour, int(match[5]||0) - tzMin, int(match[6]||0), int(match[7]||0)); + dateSetter.call(date, int(match[1]), int(match[2]) - 1, int(match[3])); + var h = int(match[4]||0) - tzHour; + var m = int(match[5]||0) - tzMin; + var s = int(match[6]||0); + var ms = Math.round(parseFloat('0.' + (match[7]||0)) * 1000); + timeSetter.call(date, h, m, s, ms); return date; } return string; @@ -10144,11 +15355,7 @@ function dateFilter($locale) { format = format || 'mediumDate'; format = $locale.DATETIME_FORMATS[format] || format; if (isString(date)) { - if (NUMBER_STRING.test(date)) { - date = int(date); - } else { - date = jsonStringToDate(date); - } + date = NUMBER_STRING.test(date) ? int(date) : jsonStringToDate(date); } if (isNumber(date)) { @@ -10183,8 +15390,8 @@ function dateFilter($locale) { /** * @ngdoc filter - * @name ng.filter:json - * @function + * @name json + * @kind function * * @description * Allows you to convert a JavaScript object into JSON string. @@ -10196,17 +15403,17 @@ function dateFilter($locale) { * @returns {string} JSON string. * * - * @example: - - + * @example + +
{{ {'name':'value'} | json }}
-
- + + it('should jsonify filtered objects', function() { - expect(binding("{'name':'value'}")).toMatch(/\{\n "name": ?"value"\n}/); + expect(element(by.binding("{'name':'value'}")).getText()).toMatch(/\{\n "name": ?"value"\n}/); }); - -
+ + * */ function jsonFilter() { @@ -10218,8 +15425,8 @@ function jsonFilter() { /** * @ngdoc filter - * @name ng.filter:lowercase - * @function + * @name lowercase + * @kind function * @description * Converts string to lowercase. * @see angular.lowercase @@ -10229,8 +15436,8 @@ var lowercaseFilter = valueFn(lowercase); /** * @ngdoc filter - * @name ng.filter:uppercase - * @function + * @name uppercase + * @kind function * @description * Converts string to uppercase. * @see angular.uppercase @@ -10238,141 +15445,177 @@ var lowercaseFilter = valueFn(lowercase); var uppercaseFilter = valueFn(uppercase); /** - * @ngdoc function - * @name ng.filter:limitTo - * @function + * @ngdoc filter + * @name limitTo + * @kind function * * @description - * Creates a new array containing only a specified number of elements in an array. The elements - * are taken from either the beginning or the end of the source array, as specified by the - * value and sign (positive or negative) of `limit`. - * - * Note: This function is used to augment the `Array` type in Angular expressions. See - * {@link ng.$filter} for more information about Angular arrays. - * - * @param {Array} array Source array to be limited. - * @param {string|Number} limit The length of the returned array. If the `limit` number is - * positive, `limit` number of items from the beginning of the source array are copied. - * If the number is negative, `limit` number of items from the end of the source array are - * copied. The `limit` will be trimmed if it exceeds `array.length` - * @returns {Array} A new sub-array of length `limit` or less if input array had less than `limit` - * elements. + * Creates a new array or string containing only a specified number of elements. The elements + * are taken from either the beginning or the end of the source array or string, as specified by + * the value and sign (positive or negative) of `limit`. + * + * @param {Array|string} input Source array or string to be limited. + * @param {string|number} limit The length of the returned array or string. If the `limit` number + * is positive, `limit` number of items from the beginning of the source array/string are copied. + * If the number is negative, `limit` number of items from the end of the source array/string + * are copied. The `limit` will be trimmed if it exceeds `array.length` + * @returns {Array|string} A new sub-array or substring of length `limit` or less if input array + * had less than `limit` elements. * * @example - - + + -
- Limit {{numbers}} to: -

Output: {{ numbers | limitTo:limit }}

+
+ Limit {{numbers}} to: +

Output numbers: {{ numbers | limitTo:numLimit }}

+ Limit {{letters}} to: +

Output letters: {{ letters | limitTo:letterLimit }}

- - - it('should limit the numer array to first three items', function() { - expect(element('.doc-example-live input[ng-model=limit]').val()).toBe('3'); - expect(binding('numbers | limitTo:limit')).toEqual('[1,2,3]'); + + + var numLimitInput = element(by.model('numLimit')); + var letterLimitInput = element(by.model('letterLimit')); + var limitedNumbers = element(by.binding('numbers | limitTo:numLimit')); + var limitedLetters = element(by.binding('letters | limitTo:letterLimit')); + + it('should limit the number array to first three items', function() { + expect(numLimitInput.getAttribute('value')).toBe('3'); + expect(letterLimitInput.getAttribute('value')).toBe('3'); + expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3]'); + expect(limitedLetters.getText()).toEqual('Output letters: abc'); }); - it('should update the output when -3 is entered', function() { - input('limit').enter(-3); - expect(binding('numbers | limitTo:limit')).toEqual('[7,8,9]'); - }); + // There is a bug in safari and protractor that doesn't like the minus key + // it('should update the output when -3 is entered', function() { + // numLimitInput.clear(); + // numLimitInput.sendKeys('-3'); + // letterLimitInput.clear(); + // letterLimitInput.sendKeys('-3'); + // expect(limitedNumbers.getText()).toEqual('Output numbers: [7,8,9]'); + // expect(limitedLetters.getText()).toEqual('Output letters: ghi'); + // }); it('should not exceed the maximum size of input array', function() { - input('limit').enter(100); - expect(binding('numbers | limitTo:limit')).toEqual('[1,2,3,4,5,6,7,8,9]'); + numLimitInput.clear(); + numLimitInput.sendKeys('100'); + letterLimitInput.clear(); + letterLimitInput.sendKeys('100'); + expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3,4,5,6,7,8,9]'); + expect(limitedLetters.getText()).toEqual('Output letters: abcdefghi'); }); - - + + */ function limitToFilter(){ - return function(array, limit) { - if (!(array instanceof Array)) return array; - limit = int(limit); + return function(input, limit) { + if (!isArray(input) && !isString(input)) return input; + + if (Math.abs(Number(limit)) === Infinity) { + limit = Number(limit); + } else { + limit = int(limit); + } + + if (isString(input)) { + //NaN check on limit + if (limit) { + return limit >= 0 ? input.slice(0, limit) : input.slice(limit, input.length); + } else { + return ""; + } + } + var out = [], i, n; - // check that array is iterable - if (!array || !(array instanceof Array)) - return out; - // if abs(limit) exceeds maximum length, trim it - if (limit > array.length) - limit = array.length; - else if (limit < -array.length) - limit = -array.length; + if (limit > input.length) + limit = input.length; + else if (limit < -input.length) + limit = -input.length; if (limit > 0) { i = 0; n = limit; } else { - i = array.length + limit; - n = array.length; + i = input.length + limit; + n = input.length; } for (; i} expression A predicate to be + * @param {function(*)|string|Array.<(function(*)|string)>=} expression A predicate to be * used by the comparator to determine the order of elements. * * Can be one of: * * - `function`: Getter function. The result of this function will be sorted using the * `<`, `=`, `>` operator. - * - `string`: An Angular expression which evaluates to an object to order by, such as 'name' - * to sort by a property called 'name'. Optionally prefixed with `+` or `-` to control - * ascending or descending sort order (for example, +name or -name). + * - `string`: An Angular expression. The result of this expression is used to compare elements + * (for example `name` to sort by a property called `name` or `name.substr(0, 3)` to sort by + * 3 first characters of a property called `name`). The result of a constant expression + * is interpreted as a property name to be used in comparisons (for example `"special name"` + * to sort object by the value of their `special name` property). An expression can be + * optionally prefixed with `+` or `-` to control ascending or descending sort order + * (for example, `+name` or `-name`). If no property is provided, (e.g. `'+'`) then the array + * element itself is used to compare where sorting. * - `Array`: An array of function or string predicates. The first predicate in the array * is used for sorting, but when two items are equivalent, the next predicate is used. * - * @param {boolean=} reverse Reverse the order the array. + * If the predicate is missing or empty then it defaults to `'+'`. + * + * @param {boolean=} reverse Reverse the order of the array. * @returns {Array} Sorted copy of the source array. * * @example - - + + -
+
Sorting predicate = {{predicate}}; reverse = {{reverse}}

[ unsorted ] + (^) @@ -10383,38 +15626,60 @@ function limitToFilter(){
Name - (^) Phone Number Age
- - - it('should be reverse ordered by aged', function() { - expect(binding('predicate')).toBe('-age'); - expect(repeater('table.friend', 'friend in friends').column('friend.age')). - toEqual(['35', '29', '21', '19', '10']); - expect(repeater('table.friend', 'friend in friends').column('friend.name')). - toEqual(['Adam', 'Julie', 'Mike', 'Mary', 'John']); - }); + + + * + * It's also possible to call the orderBy filter manually, by injecting `$filter`, retrieving the + * filter routine with `$filter('orderBy')`, and calling the returned filter routine with the + * desired parameters. + * + * Example: + * + * @example + + +
+ + + + + + + + + + + +
Name + (^)Phone NumberAge
{{friend.name}}{{friend.phone}}{{friend.age}}
+
+
- it('should reorder the table when user selects different predicate', function() { - element('.doc-example-live a:contains("Name")').click(); - expect(repeater('table.friend', 'friend in friends').column('friend.name')). - toEqual(['Adam', 'John', 'Julie', 'Mary', 'Mike']); - expect(repeater('table.friend', 'friend in friends').column('friend.age')). - toEqual(['35', '10', '29', '19', '21']); - - element('.doc-example-live a:contains("Phone")').click(); - expect(repeater('table.friend', 'friend in friends').column('friend.phone')). - toEqual(['555-9876', '555-8765', '555-5678', '555-4321', '555-1212']); - expect(repeater('table.friend', 'friend in friends').column('friend.name')). - toEqual(['Mary', 'Julie', 'Adam', 'Mike', 'John']); - }); -
- + + angular.module('orderByExample', []) + .controller('ExampleController', ['$scope', '$filter', function($scope, $filter) { + var orderBy = $filter('orderBy'); + $scope.friends = [ + { name: 'John', phone: '555-1212', age: 10 }, + { name: 'Mary', phone: '555-9876', age: 19 }, + { name: 'Mike', phone: '555-4321', age: 21 }, + { name: 'Adam', phone: '555-5678', age: 35 }, + { name: 'Julie', phone: '555-8765', age: 29 } + ]; + $scope.order = function(predicate, reverse) { + $scope.friends = orderBy($scope.friends, predicate, reverse); + }; + $scope.order('-age',false); + }]); + + */ orderByFilter.$inject = ['$parse']; function orderByFilter($parse){ return function(array, sortPredicate, reverseOrder) { - if (!isArray(array)) return array; - if (!sortPredicate) return array; + if (!(isArrayLike(array))) return array; sortPredicate = isArray(sortPredicate) ? sortPredicate: [sortPredicate]; + if (sortPredicate.length === 0) { sortPredicate = ['+']; } sortPredicate = map(sortPredicate, function(predicate){ var descending = false, get = predicate || identity; if (isString(predicate)) { @@ -10422,15 +15687,25 @@ function orderByFilter($parse){ descending = predicate.charAt(0) == '-'; predicate = predicate.substring(1); } + if ( predicate === '' ) { + // Effectively no predicate was passed so we compare identity + return reverseComparator(function(a,b) { + return compare(a, b); + }, descending); + } get = $parse(predicate); + if (get.constant) { + var key = get(); + return reverseComparator(function(a,b) { + return compare(a[key], b[key]); + }, descending); + } } return reverseComparator(function(a,b){ return compare(get(a),get(b)); }, descending); }); - var arrayCopy = []; - for ( var i = 0; i < array.length; i++) { arrayCopy.push(array[i]); } - return arrayCopy.sort(reverseComparator(comparator, reverseOrder)); + return slice.call(array).sort(reverseComparator(comparator, reverseOrder)); function comparator(o1, o2){ for ( var i = 0; i < sortPredicate.length; i++) { @@ -10448,22 +15723,28 @@ function orderByFilter($parse){ var t1 = typeof v1; var t2 = typeof v2; if (t1 == t2) { - if (t1 == "string") v1 = v1.toLowerCase(); - if (t1 == "string") v2 = v2.toLowerCase(); + if (isDate(v1) && isDate(v2)) { + v1 = v1.valueOf(); + v2 = v2.valueOf(); + } + if (t1 == "string") { + v1 = v1.toLowerCase(); + v2 = v2.toLowerCase(); + } if (v1 === v2) return 0; return v1 < v2 ? -1 : 1; } else { return t1 < t2 ? -1 : 1; } } - } + }; } function ngDirective(directive) { if (isFunction(directive)) { directive = { link: directive - } + }; } directive.restrict = directive.restrict || 'AC'; return valueFn(directive); @@ -10471,16 +15752,16 @@ function ngDirective(directive) { /** * @ngdoc directive - * @name ng.directive:a + * @name a * @restrict E * * @description - * Modifies the default behavior of html A tag, so that the default action is prevented when href - * attribute is empty. + * Modifies the default behavior of the html A tag so that the default action is prevented when + * the href attribute is empty. * - * The reasoning for this change is to allow easy creation of action links with `ngClick` directive + * This change permits the easy creation of action links with the `ngClick` directive * without changing the location or causing page reloads, e.g.: - * `Save` + * `Add Item` */ var htmlAnchorDirective = valueFn({ restrict: 'E', @@ -10501,46 +15782,55 @@ var htmlAnchorDirective = valueFn({ element.append(document.createComment('IE fix')); } - return function(scope, element) { - element.bind('click', function(event){ - // if we have no href url, then don't navigate anywhere. - if (!element.attr('href')) { - event.preventDefault(); - } - }); + if (!attr.href && !attr.xlinkHref && !attr.name) { + return function(scope, element) { + // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute. + var href = toString.call(element.prop('href')) === '[object SVGAnimatedString]' ? + 'xlink:href' : 'href'; + element.on('click', function(event){ + // if we have no href url, then don't navigate anywhere. + if (!element.attr(href)) { + event.preventDefault(); + } + }); + }; } } }); /** * @ngdoc directive - * @name ng.directive:ngHref + * @name ngHref * @restrict A + * @priority 99 * * @description - * Using Angular markup like {{hash}} in an href attribute makes - * the page open to a wrong URL, if the user clicks that link before - * angular has a chance to replace the {{hash}} with actual URL, the - * link will be broken and will most likely return a 404 error. + * Using Angular markup like `{{hash}}` in an href attribute will + * make the link go to the wrong URL if the user clicks it before + * Angular has a chance to replace the `{{hash}}` markup with its + * value. Until Angular replaces the markup the link will be broken + * and will most likely return a 404 error. + * * The `ngHref` directive solves this problem. * - * The buggy way to write it: - *
+ * The wrong way to write it:
+ * ```html
  * 
- * 
+ * ``` * * The correct way to write it: - *
+ * ```html
  * 
- * 
+ * ``` * * @element A * @param {template} ngHref any string which can contain `{{}}` markup. * * @example - * This example uses `link` variable inside `href` attribute: - - + * This example shows various combinations of `href`, `ng-href` and `ng-click` attributes + * in links and their different behaviors: + +
link 1 (link, don't reload)
link 2 (link, don't reload)
@@ -10548,54 +15838,71 @@ var htmlAnchorDirective = valueFn({ anchor (link, don't reload)
anchor (no link)
link (link, change location) - - + + it('should execute ng-click but not reload when href without value', function() { - element('#link-1').click(); - expect(input('value').val()).toEqual('1'); - expect(element('#link-1').attr('href')).toBe(""); + element(by.id('link-1')).click(); + expect(element(by.model('value')).getAttribute('value')).toEqual('1'); + expect(element(by.id('link-1')).getAttribute('href')).toBe(''); }); it('should execute ng-click but not reload when href empty string', function() { - element('#link-2').click(); - expect(input('value').val()).toEqual('2'); - expect(element('#link-2').attr('href')).toBe(""); + element(by.id('link-2')).click(); + expect(element(by.model('value')).getAttribute('value')).toEqual('2'); + expect(element(by.id('link-2')).getAttribute('href')).toBe(''); }); it('should execute ng-click and change url when ng-href specified', function() { - expect(element('#link-3').attr('href')).toBe("/123"); + expect(element(by.id('link-3')).getAttribute('href')).toMatch(/\/123$/); - element('#link-3').click(); - expect(browser().window().path()).toEqual('/123'); + element(by.id('link-3')).click(); + + // At this point, we navigate away from an Angular page, so we need + // to use browser.driver to get the base webdriver. + + browser.wait(function() { + return browser.driver.getCurrentUrl().then(function(url) { + return url.match(/\/123$/); + }); + }, 5000, 'page should navigate to /123'); }); - it('should execute ng-click but not reload when href empty string and name specified', function() { - element('#link-4').click(); - expect(input('value').val()).toEqual('4'); - expect(element('#link-4').attr('href')).toBe(''); + xit('should execute ng-click but not reload when href empty string and name specified', function() { + element(by.id('link-4')).click(); + expect(element(by.model('value')).getAttribute('value')).toEqual('4'); + expect(element(by.id('link-4')).getAttribute('href')).toBe(''); }); it('should execute ng-click but not reload when no href but name specified', function() { - element('#link-5').click(); - expect(input('value').val()).toEqual('5'); - expect(element('#link-5').attr('href')).toBe(undefined); + element(by.id('link-5')).click(); + expect(element(by.model('value')).getAttribute('value')).toEqual('5'); + expect(element(by.id('link-5')).getAttribute('href')).toBe(null); }); it('should only change url when only ng-href', function() { - input('value').enter('6'); - expect(element('#link-6').attr('href')).toBe('6'); + element(by.model('value')).clear(); + element(by.model('value')).sendKeys('6'); + expect(element(by.id('link-6')).getAttribute('href')).toMatch(/\/6$/); + + element(by.id('link-6')).click(); - element('#link-6').click(); - expect(browser().location().url()).toEqual('/6'); + // At this point, we navigate away from an Angular page, so we need + // to use browser.driver to get the base webdriver. + browser.wait(function() { + return browser.driver.getCurrentUrl().then(function(url) { + return url.match(/\/6$/); + }); + }, 5000, 'page should navigate to /6'); }); - - + + */ /** * @ngdoc directive - * @name ng.directive:ngSrc + * @name ngSrc * @restrict A + * @priority 99 * * @description * Using Angular markup like `{{hash}}` in a `src` attribute doesn't @@ -10604,14 +15911,14 @@ var htmlAnchorDirective = valueFn({ * `{{hash}}`. The `ngSrc` directive solves this problem. * * The buggy way to write it: - *
+ * ```html
  * 
- * 
+ * ``` * * The correct way to write it: - *
+ * ```html
  * 
- * 
+ * ``` * * @element IMG * @param {template} ngSrc any string which can contain `{{}}` markup. @@ -10619,227 +15926,290 @@ var htmlAnchorDirective = valueFn({ /** * @ngdoc directive - * @name ng.directive:ngDisabled + * @name ngSrcset * @restrict A + * @priority 99 * * @description + * Using Angular markup like `{{hash}}` in a `srcset` attribute doesn't + * work right: The browser will fetch from the URL with the literal + * text `{{hash}}` until Angular replaces the expression inside + * `{{hash}}`. The `ngSrcset` directive solves this problem. * - * The following markup will make the button enabled on Chrome/Firefox but not on IE8 and older IEs: - *
+ * The buggy way to write it:
+ * ```html
+ * 
+ * ```
+ *
+ * The correct way to write it:
+ * ```html
+ * 
+ * ```
+ *
+ * @element IMG
+ * @param {template} ngSrcset any string which can contain `{{}}` markup.
+ */
+
+/**
+ * @ngdoc directive
+ * @name ngDisabled
+ * @restrict A
+ * @priority 100
+ *
+ * @description
+ *
+ * We shouldn't do this, because it will make the button enabled on Chrome/Firefox but not on IE8 and older IEs:
+ * ```html
  * 
* *
- *
+ * ``` * - * The HTML specs do not require browsers to preserve the special attributes such as disabled. - * (The presence of them means true and absence means false) - * This prevents the angular compiler from correctly retrieving the binding expression. - * To solve this problem, we introduce the `ngDisabled` directive. + * The HTML specification does not require browsers to preserve the values of boolean attributes + * such as disabled. (Their presence means true and their absence means false.) + * If we put an Angular interpolation expression into such an attribute then the + * binding information would be lost when the browser removes the attribute. + * The `ngDisabled` directive solves this problem for the `disabled` attribute. + * This complementary directive is not removed by the browser and so provides + * a permanent reliable place to store the binding information. * * @example - - + + Click me to toggle:
-
- + + it('should toggle button', function() { - expect(element('.doc-example-live :button').prop('disabled')).toBeFalsy(); - input('checked').check(); - expect(element('.doc-example-live :button').prop('disabled')).toBeTruthy(); + expect(element(by.css('button')).getAttribute('disabled')).toBeFalsy(); + element(by.model('checked')).click(); + expect(element(by.css('button')).getAttribute('disabled')).toBeTruthy(); }); - -
+ + * * @element INPUT - * @param {expression} ngDisabled Angular expression that will be evaluated. + * @param {expression} ngDisabled If the {@link guide/expression expression} is truthy, + * then special attribute "disabled" will be set on the element */ /** * @ngdoc directive - * @name ng.directive:ngChecked + * @name ngChecked * @restrict A + * @priority 100 * * @description - * The HTML specs do not require browsers to preserve the special attributes such as checked. - * (The presence of them means true and absence means false) - * This prevents the angular compiler from correctly retrieving the binding expression. - * To solve this problem, we introduce the `ngChecked` directive. + * The HTML specification does not require browsers to preserve the values of boolean attributes + * such as checked. (Their presence means true and their absence means false.) + * If we put an Angular interpolation expression into such an attribute then the + * binding information would be lost when the browser removes the attribute. + * The `ngChecked` directive solves this problem for the `checked` attribute. + * This complementary directive is not removed by the browser and so provides + * a permanent reliable place to store the binding information. * @example - - + + Check me to check both:
-
- + + it('should check both checkBoxes', function() { - expect(element('.doc-example-live #checkSlave').prop('checked')).toBeFalsy(); - input('master').check(); - expect(element('.doc-example-live #checkSlave').prop('checked')).toBeTruthy(); + expect(element(by.id('checkSlave')).getAttribute('checked')).toBeFalsy(); + element(by.model('master')).click(); + expect(element(by.id('checkSlave')).getAttribute('checked')).toBeTruthy(); }); - -
+ + * * @element INPUT - * @param {expression} ngChecked Angular expression that will be evaluated. + * @param {expression} ngChecked If the {@link guide/expression expression} is truthy, + * then special attribute "checked" will be set on the element */ /** * @ngdoc directive - * @name ng.directive:ngMultiple + * @name ngReadonly * @restrict A + * @priority 100 * * @description - * The HTML specs do not require browsers to preserve the special attributes such as multiple. - * (The presence of them means true and absence means false) - * This prevents the angular compiler from correctly retrieving the binding expression. - * To solve this problem, we introduce the `ngMultiple` directive. - * - * @example - - - Check me check multiple:
- -
- - it('should toggle multiple', function() { - expect(element('.doc-example-live #select').prop('multiple')).toBeFalsy(); - input('checked').check(); - expect(element('.doc-example-live #select').prop('multiple')).toBeTruthy(); - }); - -
- * - * @element SELECT - * @param {expression} ngMultiple Angular expression that will be evaluated. - */ - - -/** - * @ngdoc directive - * @name ng.directive:ngReadonly - * @restrict A - * - * @description - * The HTML specs do not require browsers to preserve the special attributes such as readonly. - * (The presence of them means true and absence means false) - * This prevents the angular compiler from correctly retrieving the binding expression. - * To solve this problem, we introduce the `ngReadonly` directive. + * The HTML specification does not require browsers to preserve the values of boolean attributes + * such as readonly. (Their presence means true and their absence means false.) + * If we put an Angular interpolation expression into such an attribute then the + * binding information would be lost when the browser removes the attribute. + * The `ngReadonly` directive solves this problem for the `readonly` attribute. + * This complementary directive is not removed by the browser and so provides + * a permanent reliable place to store the binding information. * @example - - + + Check me to make text readonly:
-
- + + it('should toggle readonly attr', function() { - expect(element('.doc-example-live :text').prop('readonly')).toBeFalsy(); - input('checked').check(); - expect(element('.doc-example-live :text').prop('readonly')).toBeTruthy(); + expect(element(by.css('[type="text"]')).getAttribute('readonly')).toBeFalsy(); + element(by.model('checked')).click(); + expect(element(by.css('[type="text"]')).getAttribute('readonly')).toBeTruthy(); }); - -
+ + * * @element INPUT - * @param {string} expression Angular expression that will be evaluated. + * @param {expression} ngReadonly If the {@link guide/expression expression} is truthy, + * then special attribute "readonly" will be set on the element */ /** * @ngdoc directive - * @name ng.directive:ngSelected + * @name ngSelected * @restrict A + * @priority 100 * * @description - * The HTML specs do not require browsers to preserve the special attributes such as selected. - * (The presence of them means true and absence means false) - * This prevents the angular compiler from correctly retrieving the binding expression. - * To solve this problem, we introduced the `ngSelected` directive. + * The HTML specification does not require browsers to preserve the values of boolean attributes + * such as selected. (Their presence means true and their absence means false.) + * If we put an Angular interpolation expression into such an attribute then the + * binding information would be lost when the browser removes the attribute. + * The `ngSelected` directive solves this problem for the `selected` attribute. + * This complementary directive is not removed by the browser and so provides + * a permanent reliable place to store the binding information. + * * @example - - + + Check me to select:
-
- + + it('should select Greetings!', function() { - expect(element('.doc-example-live #greet').prop('selected')).toBeFalsy(); - input('selected').check(); - expect(element('.doc-example-live #greet').prop('selected')).toBeTruthy(); + expect(element(by.id('greet')).getAttribute('selected')).toBeFalsy(); + element(by.model('selected')).click(); + expect(element(by.id('greet')).getAttribute('selected')).toBeTruthy(); }); - -
+ + * * @element OPTION - * @param {string} expression Angular expression that will be evaluated. + * @param {expression} ngSelected If the {@link guide/expression expression} is truthy, + * then special attribute "selected" will be set on the element */ +/** + * @ngdoc directive + * @name ngOpen + * @restrict A + * @priority 100 + * + * @description + * The HTML specification does not require browsers to preserve the values of boolean attributes + * such as open. (Their presence means true and their absence means false.) + * If we put an Angular interpolation expression into such an attribute then the + * binding information would be lost when the browser removes the attribute. + * The `ngOpen` directive solves this problem for the `open` attribute. + * This complementary directive is not removed by the browser and so provides + * a permanent reliable place to store the binding information. + * @example + + + Check me check multiple:
+
+ Show/Hide me +
+
+ + it('should toggle open', function() { + expect(element(by.id('details')).getAttribute('open')).toBeFalsy(); + element(by.model('open')).click(); + expect(element(by.id('details')).getAttribute('open')).toBeTruthy(); + }); + +
+ * + * @element DETAILS + * @param {expression} ngOpen If the {@link guide/expression expression} is truthy, + * then special attribute "open" will be set on the element + */ var ngAttributeAliasDirectives = {}; // boolean attrs are evaluated forEach(BOOLEAN_ATTR, function(propName, attrName) { + // binding to multiple is not supported + if (propName == "multiple") return; + var normalized = directiveNormalize('ng-' + attrName); ngAttributeAliasDirectives[normalized] = function() { return { priority: 100, - compile: function() { - return function(scope, element, attr) { - scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) { - attr.$set(attrName, !!value); - }); - }; + link: function(scope, element, attr) { + scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) { + attr.$set(attrName, !!value); + }); } }; }; }); -// ng-src, ng-href are interpolated -forEach(['src', 'href'], function(attrName) { +// ng-src, ng-srcset, ng-href are interpolated +forEach(['src', 'srcset', 'href'], function(attrName) { var normalized = directiveNormalize('ng-' + attrName); ngAttributeAliasDirectives[normalized] = function() { return { priority: 99, // it needs to run after the attributes are interpolated link: function(scope, element, attr) { + var propName = attrName, + name = attrName; + + if (attrName === 'href' && + toString.call(element.prop('href')) === '[object SVGAnimatedString]') { + name = 'xlinkHref'; + attr.$attr[name] = 'xlink:href'; + propName = null; + } + attr.$observe(normalized, function(value) { - if (!value) - return; + if (!value) { + if (attrName === 'href') { + attr.$set(name, null); + } + return; + } - attr.$set(attrName, value); + attr.$set(name, value); // on IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist // then calling element.setAttribute('src', 'foo') doesn't do anything, so we need // to set the property as well to achieve the desired effect. // we use attr[attrName] value since $set can sanitize the url. - if (msie) element.prop(attrName, attr[attrName]); + if (msie && propName) element.prop(propName, attr[name]); }); } }; }; }); +/* global -nullFormCtrl */ var nullFormCtrl = { $addControl: noop, $removeControl: noop, $setValidity: noop, - $setDirty: noop + $setDirty: noop, + $setPristine: noop }; /** - * @ngdoc object - * @name ng.directive:form.FormController + * @ngdoc type + * @name form.FormController * * @property {boolean} $pristine True if user has not interacted with the form yet. * @property {boolean} $dirty True if user has already interacted with the form. @@ -10849,11 +16219,24 @@ var nullFormCtrl = { * @property {Object} $error Is an object hash, containing references to all invalid controls or * forms, where: * - * - keys are validation tokens (error names) — such as `required`, `url` or `email`), - * - values are arrays of controls or forms that are invalid with given error. + * - keys are validation tokens (error names), + * - values are arrays of controls or forms that are invalid for given error name. + * + * + * Built-in validation tokens: + * + * - `email` + * - `max` + * - `maxlength` + * - `min` + * - `minlength` + * - `number` + * - `pattern` + * - `required` + * - `url` * * @description - * `FormController` keeps track of all its controls and nested forms as well as state of them, + * `FormController` keeps track of all its controls and nested forms as well as the state of them, * such as being valid/invalid or dirty/pristine. * * Each {@link ng.directive:form form} directive creates an instance @@ -10861,15 +16244,16 @@ var nullFormCtrl = { * */ //asks for $scope to fool the BC controller module -FormController.$inject = ['$element', '$attrs', '$scope']; -function FormController(element, attrs) { +FormController.$inject = ['$element', '$attrs', '$scope', '$animate']; +function FormController(element, attrs, $scope, $animate) { var form = this, parentForm = element.parent().controller('form') || nullFormCtrl, invalidCount = 0, // used to easily determine if we are valid - errors = form.$error = {}; + errors = form.$error = {}, + controls = []; // init state - form.$name = attrs.name; + form.$name = attrs.name || attrs.ngForm; form.$dirty = false; form.$pristine = true; form.$valid = true; @@ -10884,17 +16268,40 @@ function FormController(element, attrs) { // convenience method for easy toggling of classes function toggleValidCss(isValid, validationErrorKey) { validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; - element. - removeClass((isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey). - addClass((isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); + $animate.setClass(element, + (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey, + (isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey); } + /** + * @ngdoc method + * @name form.FormController#$addControl + * + * @description + * Register a control with the form. + * + * Input elements using ngModelController do this automatically when they are linked. + */ form.$addControl = function(control) { - if (control.$name && !form.hasOwnProperty(control.$name)) { + // Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored + // and not added to the scope. Now we throw an error. + assertNotHasOwnProperty(control.$name, 'input'); + controls.push(control); + + if (control.$name) { form[control.$name] = control; } }; + /** + * @ngdoc method + * @name form.FormController#$removeControl + * + * @description + * Deregister a control from the form. + * + * Input elements using ngModelController do this automatically when they are destroyed. + */ form.$removeControl = function(control) { if (control.$name && form[control.$name] === control) { delete form[control.$name]; @@ -10902,8 +16309,19 @@ function FormController(element, attrs) { forEach(errors, function(queue, validationToken) { form.$setValidity(validationToken, true, control); }); + + arrayRemove(controls, control); }; + /** + * @ngdoc method + * @name form.FormController#$setValidity + * + * @description + * Sets the validity of a form control. + * + * This method will also propagate to parent forms. + */ form.$setValidity = function(validationToken, isValid, control) { var queue = errors[validationToken]; @@ -10942,19 +16360,53 @@ function FormController(element, attrs) { } }; + /** + * @ngdoc method + * @name form.FormController#$setDirty + * + * @description + * Sets the form to a dirty state. + * + * This method can be called to add the 'ng-dirty' class and set the form to a dirty + * state (ng-dirty class). This method will also propagate to parent forms. + */ form.$setDirty = function() { - element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS); + $animate.removeClass(element, PRISTINE_CLASS); + $animate.addClass(element, DIRTY_CLASS); form.$dirty = true; form.$pristine = false; parentForm.$setDirty(); }; + /** + * @ngdoc method + * @name form.FormController#$setPristine + * + * @description + * Sets the form to its pristine state. + * + * This method can be called to remove the 'ng-dirty' class and set the form to its pristine + * state (ng-pristine class). This method will also propagate to all the controls contained + * in this form. + * + * Setting a form back to a pristine state is often useful when we want to 'reuse' a form after + * saving or resetting it. + */ + form.$setPristine = function () { + $animate.removeClass(element, DIRTY_CLASS); + $animate.addClass(element, PRISTINE_CLASS); + form.$dirty = false; + form.$pristine = true; + forEach(controls, function(control) { + control.$setPristine(); + }); + }; } /** * @ngdoc directive - * @name ng.directive:ngForm + * @name ngForm * @restrict EAC * * @description @@ -10962,44 +16414,54 @@ function FormController(element, attrs) { * does not allow nesting of form elements. It is useful to nest forms, for example if the validity of a * sub-group of controls needs to be determined. * - * @param {string=} name|ngForm Name of the form. If specified, the form controller will be published into + * Note: the purpose of `ngForm` is to group controls, + * but not to be a replacement for the `
` tag with all of its capabilities + * (e.g. posting to the server, ...). + * + * @param {string=} ngForm|name Name of the form. If specified, the form controller will be published into * related scope, under this name. * */ /** * @ngdoc directive - * @name ng.directive:form + * @name form * @restrict E * * @description * Directive that instantiates - * {@link ng.directive:form.FormController FormController}. + * {@link form.FormController FormController}. * - * If `name` attribute is specified, the form controller is published onto the current scope under + * If the `name` attribute is specified, the form controller is published onto the current scope under * this name. * * # Alias: {@link ng.directive:ngForm `ngForm`} * - * In angular forms can be nested. This means that the outer form is valid when all of the child - * forms are valid as well. However browsers do not allow nesting of `` elements, for this - * reason angular provides {@link ng.directive:ngForm `ngForm`} alias - * which behaves identical to `` but allows form nesting. + * In Angular forms can be nested. This means that the outer form is valid when all of the child + * forms are valid as well. However, browsers do not allow nesting of `` elements, so + * Angular provides the {@link ng.directive:ngForm `ngForm`} directive which behaves identically to + * `` but can be nested. This allows you to have nested forms, which is very useful when + * using Angular validation directives in forms that are dynamically generated using the + * {@link ng.directive:ngRepeat `ngRepeat`} directive. Since you cannot dynamically generate the `name` + * attribute of input elements using interpolation, you have to wrap each set of repeated inputs in an + * `ngForm` directive and nest these in an outer `form` element. * * * # CSS classes - * - `ng-valid` Is set if the form is valid. - * - `ng-invalid` Is set if the form is invalid. - * - `ng-pristine` Is set if the form is pristine. - * - `ng-dirty` Is set if the form is dirty. + * - `ng-valid` is set if the form is valid. + * - `ng-invalid` is set if the form is invalid. + * - `ng-pristine` is set if the form is pristine. + * - `ng-dirty` is set if the form is dirty. * + * Keep in mind that ngAnimate can detect each of these classes when added and removed. * - * # Submitting a form and preventing default action + * + * # Submitting a form and preventing the default action * * Since the role of forms in client-side Angular applications is different than in classical * roundtrip apps, it is desirable for the browser not to translate the form submission into a full * page reload that sends the data to the server. Instead some javascript logic should be triggered - * to handle the form submission in application specific way. + * to handle the form submission in an application-specific way. * * For this reason, Angular prevents the default action (form submission to the server) unless the * `` element has an `action` attribute specified. @@ -11011,29 +16473,63 @@ function FormController(element, attrs) { * - {@link ng.directive:ngClick ngClick} directive on the first * button or input field of type submit (input[type=submit]) * - * To prevent double execution of the handler, use only one of ngSubmit or ngClick directives. This - * is because of the following form submission rules coming from the html spec: + * To prevent double execution of the handler, use only one of the {@link ng.directive:ngSubmit ngSubmit} + * or {@link ng.directive:ngClick ngClick} directives. + * This is because of the following form submission rules in the HTML specification: * * - If a form has only one input field then hitting enter in this field triggers form submit * (`ngSubmit`) - * - if a form has has 2+ input fields and no buttons or input[type=submit] then hitting enter + * - if a form has 2+ input fields and no buttons or input[type=submit] then hitting enter * doesn't trigger submit * - if a form has one or more input fields and one or more buttons or input[type=submit] then * hitting enter in any of the input fields will trigger the click handler on the *first* button or * input[type=submit] (`ngClick`) *and* a submit handler on the enclosing form (`ngSubmit`) * - * @param {string=} name Name of the form. If specified, the form controller will be published into - * related scope, under this name. + * + * ## Animation Hooks + * + * Animations in ngForm are triggered when any of the associated CSS classes are added and removed. + * These classes are: `.ng-pristine`, `.ng-dirty`, `.ng-invalid` and `.ng-valid` as well as any + * other validations that are performed within the form. Animations in ngForm are similar to how + * they work in ngClass and animations can be hooked into using CSS transitions, keyframes as well + * as JS animations. + * + * The following example shows a simple way to utilize CSS transitions to style a form element + * that has been rendered as invalid after it has been validated: + * + *
+ * //be sure to include ngAnimate as a module to hook into more
+ * //advanced animations
+ * .my-form {
+ *   transition:0.5s linear all;
+ *   background: white;
+ * }
+ * .my-form.ng-invalid {
+ *   background: red;
+ *   color:white;
+ * }
+ * 
* * @example - - + + - + + userType: Required!
userType = {{userType}}
@@ -11042,26 +16538,38 @@ function FormController(element, attrs) { myForm.$valid = {{myForm.$valid}}
myForm.$error.required = {{!!myForm.$error.required}}
-
- + + it('should initialize to model', function() { - expect(binding('userType')).toEqual('guest'); - expect(binding('myForm.input.$valid')).toEqual('true'); + var userType = element(by.binding('userType')); + var valid = element(by.binding('myForm.input.$valid')); + + expect(userType.getText()).toContain('guest'); + expect(valid.getText()).toContain('true'); }); it('should be invalid if empty', function() { - input('userType').enter(''); - expect(binding('userType')).toEqual(''); - expect(binding('myForm.input.$valid')).toEqual('false'); + var userType = element(by.binding('userType')); + var valid = element(by.binding('myForm.input.$valid')); + var userInput = element(by.model('userType')); + + userInput.clear(); + userInput.sendKeys(''); + + expect(userType.getText()).toEqual('userType ='); + expect(valid.getText()).toContain('false'); }); - -
+ + + * + * @param {string=} name Name of the form. If specified, the form controller will be published into + * related scope, under this name. */ var formDirectiveFactory = function(isNgForm) { return ['$timeout', function($timeout) { var formDirective = { name: 'form', - restrict: 'E', + restrict: isNgForm ? 'EAC' : 'E', controller: FormController, compile: function() { return { @@ -11083,7 +16591,7 @@ var formDirectiveFactory = function(isNgForm) { // unregister the preventDefault listener so that we don't not leak memory but in a // way that will achieve the prevention of the default action. - formElement.bind('$destroy', function() { + formElement.on('$destroy', function() { $timeout(function() { removeEventListenerFn(formElement[0], 'submit', preventDefaultListener); }, 0, false); @@ -11094,13 +16602,13 @@ var formDirectiveFactory = function(isNgForm) { alias = attr.name || attr.ngForm; if (alias) { - scope[alias] = controller; + setter(scope, alias, controller, alias); } if (parentFormCtrl) { - formElement.bind('$destroy', function() { + formElement.on('$destroy', function() { parentFormCtrl.$removeControl(controller); if (alias) { - scope[alias] = undefined; + setter(scope, alias, undefined, alias); } extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards }); @@ -11110,25 +16618,33 @@ var formDirectiveFactory = function(isNgForm) { } }; - return isNgForm ? extend(copy(formDirective), {restrict: 'EAC'}) : formDirective; + return formDirective; }]; }; var formDirective = formDirectiveFactory(); var ngFormDirective = formDirectiveFactory(true); +/* global VALID_CLASS: true, + INVALID_CLASS: true, + PRISTINE_CLASS: true, + DIRTY_CLASS: true +*/ + var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/; -var EMAIL_REGEXP = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/; +var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/; var inputType = { /** - * @ngdoc inputType - * @name ng.directive:input.text + * @ngdoc input + * @name input[text] * * @description - * Standard HTML text input with angular data binding. + * Standard HTML text input with angular data binding, inherited by most of the `input` elements. + * + * *NOTE* Not every feature offered is available for all input types. * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. @@ -11145,19 +16661,23 @@ var inputType = { * patterns defined as scope expressions. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. + * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. + * This parameter is ignored for input[type=password] controls, which will never trim the + * input. * * @example - - + + -
+ Single word: + ng-pattern="word" required ng-trim="false"> Required! @@ -11169,32 +16689,40 @@ var inputType = { myForm.$valid = {{myForm.$valid}}
myForm.$error.required = {{!!myForm.$error.required}}
-
- + + + var text = element(by.binding('text')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('text')); + it('should initialize to model', function() { - expect(binding('text')).toEqual('guest'); - expect(binding('myForm.input.$valid')).toEqual('true'); + expect(text.getText()).toContain('guest'); + expect(valid.getText()).toContain('true'); }); it('should be invalid if empty', function() { - input('text').enter(''); - expect(binding('text')).toEqual(''); - expect(binding('myForm.input.$valid')).toEqual('false'); + input.clear(); + input.sendKeys(''); + + expect(text.getText()).toEqual('text ='); + expect(valid.getText()).toContain('false'); }); it('should be invalid if multi word', function() { - input('text').enter('hello world'); - expect(binding('myForm.input.$valid')).toEqual('false'); + input.clear(); + input.sendKeys('hello world'); + + expect(valid.getText()).toContain('false'); }); - -
+ + */ 'text': textInputType, /** - * @ngdoc inputType - * @name ng.directive:input.number + * @ngdoc input + * @name input[number] * * @description * Text input with number validation and transformation. Sets the `number` validation @@ -11219,19 +16747,20 @@ var inputType = { * interaction with the input element. * * @example - - + + -
+ Number: - + Required! - + Not valid number! value = {{value}}
myForm.input.$valid = {{myForm.input.$valid}}
@@ -11239,33 +16768,39 @@ var inputType = { myForm.$valid = {{myForm.$valid}}
myForm.$error.required = {{!!myForm.$error.required}}
-
- + + + var value = element(by.binding('value')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('value')); + it('should initialize to model', function() { - expect(binding('value')).toEqual('12'); - expect(binding('myForm.input.$valid')).toEqual('true'); + expect(value.getText()).toContain('12'); + expect(valid.getText()).toContain('true'); }); it('should be invalid if empty', function() { - input('value').enter(''); - expect(binding('value')).toEqual(''); - expect(binding('myForm.input.$valid')).toEqual('false'); + input.clear(); + input.sendKeys(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('false'); }); it('should be invalid if over max', function() { - input('value').enter('123'); - expect(binding('value')).toEqual(''); - expect(binding('myForm.input.$valid')).toEqual('false'); + input.clear(); + input.sendKeys('123'); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('false'); }); - -
+ + */ 'number': numberInputType, /** - * @ngdoc inputType - * @name ng.directive:input.url + * @ngdoc input + * @name input[url] * * @description * Text input with URL validation. Sets the `url` validation error key if the content is not a @@ -11288,14 +16823,15 @@ var inputType = { * interaction with the input element. * * @example - - + + -
+ URL: Required! @@ -11308,32 +16844,40 @@ var inputType = { myForm.$error.required = {{!!myForm.$error.required}}
myForm.$error.url = {{!!myForm.$error.url}}
-
- + + + var text = element(by.binding('text')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('text')); + it('should initialize to model', function() { - expect(binding('text')).toEqual('http://google.com'); - expect(binding('myForm.input.$valid')).toEqual('true'); + expect(text.getText()).toContain('http://google.com'); + expect(valid.getText()).toContain('true'); }); it('should be invalid if empty', function() { - input('text').enter(''); - expect(binding('text')).toEqual(''); - expect(binding('myForm.input.$valid')).toEqual('false'); + input.clear(); + input.sendKeys(''); + + expect(text.getText()).toEqual('text ='); + expect(valid.getText()).toContain('false'); }); it('should be invalid if not url', function() { - input('text').enter('xxx'); - expect(binding('myForm.input.$valid')).toEqual('false'); + input.clear(); + input.sendKeys('box'); + + expect(valid.getText()).toContain('false'); }); - -
+ + */ 'url': urlInputType, /** - * @ngdoc inputType - * @name ng.directive:input.email + * @ngdoc input + * @name input[email] * * @description * Text input with email validation. Sets the `email` validation error key if not a valid email @@ -11352,16 +16896,19 @@ var inputType = { * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for * patterns defined as scope expressions. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. * * @example - - + + -
+ Email: Required! @@ -11374,32 +16921,39 @@ var inputType = { myForm.$error.required = {{!!myForm.$error.required}}
myForm.$error.email = {{!!myForm.$error.email}}
-
- + + + var text = element(by.binding('text')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('text')); + it('should initialize to model', function() { - expect(binding('text')).toEqual('me@example.com'); - expect(binding('myForm.input.$valid')).toEqual('true'); + expect(text.getText()).toContain('me@example.com'); + expect(valid.getText()).toContain('true'); }); it('should be invalid if empty', function() { - input('text').enter(''); - expect(binding('text')).toEqual(''); - expect(binding('myForm.input.$valid')).toEqual('false'); + input.clear(); + input.sendKeys(''); + expect(text.getText()).toEqual('text ='); + expect(valid.getText()).toContain('false'); }); it('should be invalid if not email', function() { - input('text').enter('xxx'); - expect(binding('myForm.input.$valid')).toEqual('false'); + input.clear(); + input.sendKeys('xxx'); + + expect(valid.getText()).toContain('false'); }); - -
+ + */ 'email': emailInputType, /** - * @ngdoc inputType - * @name ng.directive:input.radio + * @ngdoc input + * @name input[radio] * * @description * HTML radio button. @@ -11409,38 +16963,49 @@ var inputType = { * @param {string=} name Property name of the form under which the control is published. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. + * @param {string} ngValue Angular expression which sets the value to which the expression should + * be set when selected. * * @example - - + + -
+ Red
- Green
+ Green
Blue
- color = {{color}}
+ color = {{color | json}}
-
- + Note that `ng-value="specialValue"` sets radio item's value to be the value of `$scope.specialValue`. + + it('should change state', function() { - expect(binding('color')).toEqual('blue'); + var color = element(by.binding('color')); - input('color').select('red'); - expect(binding('color')).toEqual('red'); + expect(color.getText()).toContain('blue'); + + element.all(by.model('color')).get(0).click(); + + expect(color.getText()).toContain('red'); }); - -
+ + */ 'radio': radioInputType, /** - * @ngdoc inputType - * @name ng.directive:input.checkbox + * @ngdoc input + * @name input[checkbox] * * @description * HTML checkbox. @@ -11453,65 +17018,149 @@ var inputType = { * interaction with the input element. * * @example - - + + -
+ Value1:
Value2:
value1 = {{value1}}
value2 = {{value2}}
-
- + + it('should change state', function() { - expect(binding('value1')).toEqual('true'); - expect(binding('value2')).toEqual('YES'); + var value1 = element(by.binding('value1')); + var value2 = element(by.binding('value2')); + + expect(value1.getText()).toContain('true'); + expect(value2.getText()).toContain('YES'); - input('value1').check(); - input('value2').check(); - expect(binding('value1')).toEqual('false'); - expect(binding('value2')).toEqual('NO'); + element(by.model('value1')).click(); + element(by.model('value2')).click(); + + expect(value1.getText()).toContain('false'); + expect(value2.getText()).toContain('NO'); }); - -
+ + */ 'checkbox': checkboxInputType, 'hidden': noop, 'button': noop, 'submit': noop, - 'reset': noop + 'reset': noop, + 'file': noop }; +// A helper function to call $setValidity and return the value / undefined, +// a pattern that is repeated a lot in the input validation logic. +function validate(ctrl, validatorName, validity, value){ + ctrl.$setValidity(validatorName, validity); + return validity ? value : undefined; +} -function isEmpty(value) { - return isUndefined(value) || value === '' || value === null || value !== value; +function testFlags(validity, flags) { + var i, flag; + if (flags) { + for (i=0; i= minlength, value); }; ctrl.$parsers.push(minLengthValidator); @@ -11604,13 +17244,7 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { if (attr.ngMaxlength) { var maxlength = int(attr.ngMaxlength); var maxLengthValidator = function(value) { - if (!isEmpty(value) && value.length > maxlength) { - ctrl.$setValidity('maxlength', false); - return undefined; - } else { - ctrl.$setValidity('maxlength', true); - return value; - } + return validate(ctrl, 'maxlength', ctrl.$isEmpty(value) || value.length <= maxlength, value); }; ctrl.$parsers.push(maxLengthValidator); @@ -11618,11 +17252,13 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { } } +var numberBadFlags = ['badInput']; + function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { textInputType(scope, element, attr, ctrl, $sniffer, $browser); ctrl.$parsers.push(function(value) { - var empty = isEmpty(value); + var empty = ctrl.$isEmpty(value); if (empty || NUMBER_REGEXP.test(value)) { ctrl.$setValidity('number', true); return value === '' ? null : (empty ? value : parseFloat(value)); @@ -11632,20 +17268,16 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { } }); + addNativeHtml5Validators(ctrl, 'number', numberBadFlags, null, ctrl.$$validityState); + ctrl.$formatters.push(function(value) { - return isEmpty(value) ? '' : '' + value; + return ctrl.$isEmpty(value) ? '' : '' + value; }); if (attr.min) { - var min = parseFloat(attr.min); var minValidator = function(value) { - if (!isEmpty(value) && value < min) { - ctrl.$setValidity('min', false); - return undefined; - } else { - ctrl.$setValidity('min', true); - return value; - } + var min = parseFloat(attr.min); + return validate(ctrl, 'min', ctrl.$isEmpty(value) || value >= min, value); }; ctrl.$parsers.push(minValidator); @@ -11653,15 +17285,9 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { } if (attr.max) { - var max = parseFloat(attr.max); var maxValidator = function(value) { - if (!isEmpty(value) && value > max) { - ctrl.$setValidity('max', false); - return undefined; - } else { - ctrl.$setValidity('max', true); - return value; - } + var max = parseFloat(attr.max); + return validate(ctrl, 'max', ctrl.$isEmpty(value) || value <= max, value); }; ctrl.$parsers.push(maxValidator); @@ -11669,14 +17295,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { } ctrl.$formatters.push(function(value) { - - if (isEmpty(value) || isNumber(value)) { - ctrl.$setValidity('number', true); - return value; - } else { - ctrl.$setValidity('number', false); - return undefined; - } + return validate(ctrl, 'number', ctrl.$isEmpty(value) || isNumber(value), value); }); } @@ -11684,13 +17303,7 @@ function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { textInputType(scope, element, attr, ctrl, $sniffer, $browser); var urlValidator = function(value) { - if (isEmpty(value) || URL_REGEXP.test(value)) { - ctrl.$setValidity('url', true); - return value; - } else { - ctrl.$setValidity('url', false); - return undefined; - } + return validate(ctrl, 'url', ctrl.$isEmpty(value) || URL_REGEXP.test(value), value); }; ctrl.$formatters.push(urlValidator); @@ -11701,13 +17314,7 @@ function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { textInputType(scope, element, attr, ctrl, $sniffer, $browser); var emailValidator = function(value) { - if (isEmpty(value) || EMAIL_REGEXP.test(value)) { - ctrl.$setValidity('email', true); - return value; - } else { - ctrl.$setValidity('email', false); - return undefined; - } + return validate(ctrl, 'email', ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value), value); }; ctrl.$formatters.push(emailValidator); @@ -11720,7 +17327,7 @@ function radioInputType(scope, element, attr, ctrl) { element.attr('name', nextUid()); } - element.bind('click', function() { + element.on('click', function() { if (element[0].checked) { scope.$apply(function() { ctrl.$setViewValue(attr.value); @@ -11743,7 +17350,7 @@ function checkboxInputType(scope, element, attr, ctrl) { if (!isString(trueValue)) trueValue = true; if (!isString(falseValue)) falseValue = false; - element.bind('click', function() { + element.on('click', function() { scope.$apply(function() { ctrl.$setViewValue(element[0].checked); }); @@ -11753,6 +17360,11 @@ function checkboxInputType(scope, element, attr, ctrl) { element[0].checked = ctrl.$viewValue; }; + // Override the standard `$isEmpty` because a value of `false` means empty in a checkbox. + ctrl.$isEmpty = function(value) { + return value !== trueValue; + }; + ctrl.$formatters.push(function(value) { return value === trueValue; }); @@ -11765,7 +17377,7 @@ function checkboxInputType(scope, element, attr, ctrl) { /** * @ngdoc directive - * @name ng.directive:textarea + * @name textarea * @restrict E * * @description @@ -11788,18 +17400,21 @@ function checkboxInputType(scope, element, attr, ctrl) { * patterns defined as scope expressions. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. + * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. */ /** * @ngdoc directive - * @name ng.directive:input + * @name input * @restrict E * * @description * HTML input element control with angular data-binding. Input control follows HTML5 input types * and polyfills the HTML5 validation behavior for older browsers. * + * *NOTE* Not every feature offered is available for all input types. + * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} required Sets `required` validation error key if the value is not entered. @@ -11813,16 +17428,20 @@ function checkboxInputType(scope, element, attr, ctrl) { * patterns defined as scope expressions. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. + * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. + * This parameter is ignored for input[type=password] controls, which will never trim the + * input. * * @example - - + + -
+
User name: @@ -11845,46 +17464,61 @@ function checkboxInputType(scope, element, attr, ctrl) { myForm.$error.minlength = {{!!myForm.$error.minlength}}
myForm.$error.maxlength = {{!!myForm.$error.maxlength}}
- - + + + var user = element(by.binding('{{user}}')); + var userNameValid = element(by.binding('myForm.userName.$valid')); + var lastNameValid = element(by.binding('myForm.lastName.$valid')); + var lastNameError = element(by.binding('myForm.lastName.$error')); + var formValid = element(by.binding('myForm.$valid')); + var userNameInput = element(by.model('user.name')); + var userLastInput = element(by.model('user.last')); + it('should initialize to model', function() { - expect(binding('user')).toEqual('{"name":"guest","last":"visitor"}'); - expect(binding('myForm.userName.$valid')).toEqual('true'); - expect(binding('myForm.$valid')).toEqual('true'); + expect(user.getText()).toContain('{"name":"guest","last":"visitor"}'); + expect(userNameValid.getText()).toContain('true'); + expect(formValid.getText()).toContain('true'); }); it('should be invalid if empty when required', function() { - input('user.name').enter(''); - expect(binding('user')).toEqual('{"last":"visitor"}'); - expect(binding('myForm.userName.$valid')).toEqual('false'); - expect(binding('myForm.$valid')).toEqual('false'); + userNameInput.clear(); + userNameInput.sendKeys(''); + + expect(user.getText()).toContain('{"last":"visitor"}'); + expect(userNameValid.getText()).toContain('false'); + expect(formValid.getText()).toContain('false'); }); it('should be valid if empty when min length is set', function() { - input('user.last').enter(''); - expect(binding('user')).toEqual('{"name":"guest","last":""}'); - expect(binding('myForm.lastName.$valid')).toEqual('true'); - expect(binding('myForm.$valid')).toEqual('true'); + userLastInput.clear(); + userLastInput.sendKeys(''); + + expect(user.getText()).toContain('{"name":"guest","last":""}'); + expect(lastNameValid.getText()).toContain('true'); + expect(formValid.getText()).toContain('true'); }); it('should be invalid if less than required min length', function() { - input('user.last').enter('xx'); - expect(binding('user')).toEqual('{"name":"guest"}'); - expect(binding('myForm.lastName.$valid')).toEqual('false'); - expect(binding('myForm.lastName.$error')).toMatch(/minlength/); - expect(binding('myForm.$valid')).toEqual('false'); + userLastInput.clear(); + userLastInput.sendKeys('xx'); + + expect(user.getText()).toContain('{"name":"guest"}'); + expect(lastNameValid.getText()).toContain('false'); + expect(lastNameError.getText()).toContain('minlength'); + expect(formValid.getText()).toContain('false'); }); it('should be invalid if longer than max length', function() { - input('user.last').enter('some ridiculously long name'); - expect(binding('user')) - .toEqual('{"name":"guest"}'); - expect(binding('myForm.lastName.$valid')).toEqual('false'); - expect(binding('myForm.lastName.$error')).toMatch(/maxlength/); - expect(binding('myForm.$valid')).toEqual('false'); + userLastInput.clear(); + userLastInput.sendKeys('some ridiculously long name'); + + expect(user.getText()).toContain('{"name":"guest"}'); + expect(lastNameValid.getText()).toContain('false'); + expect(lastNameError.getText()).toContain('maxlength'); + expect(formValid.getText()).toContain('false'); }); - - + + */ var inputDirective = ['$browser', '$sniffer', function($browser, $sniffer) { return { @@ -11905,18 +17539,37 @@ var VALID_CLASS = 'ng-valid', DIRTY_CLASS = 'ng-dirty'; /** - * @ngdoc object - * @name ng.directive:ngModel.NgModelController + * @ngdoc type + * @name ngModel.NgModelController * * @property {string} $viewValue Actual string value in the view. * @property {*} $modelValue The value in the model, that the control is bound to. - * @property {Array.} $parsers Whenever the control reads value from the DOM, it executes - * all of these functions to sanitize / convert the value as well as validate. + * @property {Array.} $parsers Array of functions to execute, as a pipeline, whenever + the control reads value from the DOM. Each function is called, in turn, passing the value + through to the next. The last return value is used to populate the model. + Used to sanitize / convert the value as well as validation. For validation, + the parsers should update the validity state using + {@link ngModel.NgModelController#$setValidity $setValidity()}, + and return `undefined` for invalid values. + + * + * @property {Array.} $formatters Array of functions to execute, as a pipeline, whenever + the model value changes. Each function is called, in turn, passing the value through to the + next. Used to format / convert values for display in the control and validation. + * ```js + * function formatter(value) { + * if (value) { + * return value.toUpperCase(); + * } + * } + * ngModel.$formatters.push(formatter); + * ``` * - * @property {Array.} $formatters Whenever the model value changes, it executes all of - * these functions to convert the value as well as validate. + * @property {Array.} $viewChangeListeners Array of functions to execute whenever the + * view value has changed. It is called with no arguments, and its return value is ignored. + * This can be used in place of additional $watches against the model value. * - * @property {Object} $error An bject hash with all errors as keys. + * @property {Object} $error An object hash with all errors as keys. * * @property {boolean} $pristine True if user has not interacted with the control yet. * @property {boolean} $dirty True if user has already interacted with the control. @@ -11926,16 +17579,25 @@ var VALID_CLASS = 'ng-valid', * @description * * `NgModelController` provides API for the `ng-model` directive. The controller contains - * services for data-binding, validation, CSS update, value formatting and parsing. It - * specifically does not contain any logic which deals with DOM rendering or listening to - * DOM events. The `NgModelController` is meant to be extended by other directives where, the - * directive provides DOM manipulation and the `NgModelController` provides the data-binding. + * services for data-binding, validation, CSS updates, and value formatting and parsing. It + * purposefully does not contain any logic which deals with DOM rendering or listening to + * DOM events. Such DOM related logic should be provided by other directives which make use of + * `NgModelController` for data-binding. * + * ## Custom Control Example * This example shows how to use `NgModelController` with a custom control to achieve * data-binding. Notice how different directives (`contenteditable`, `ng-model`, and `required`) * collaborate together to achieve the desired result. * - * + * Note that `contenteditable` is an HTML5 attribute, which tells the browser to let the element + * contents be edited in place by the user. This will not work on older browsers. + * + * We are using the {@link ng.service:$sce $sce} service here and include the {@link ngSanitize $sanitize} + * module to automatically remove "bad" content like inline event listener (e.g. ``). + * However, as we are using `$sce` the model can still decide to provide unsafe content if it marks + * that content using the `$sce` service. + * + * [contenteditable] { border: 1px solid black; @@ -11949,8 +17611,8 @@ var VALID_CLASS = 'ng-valid', - angular.module('customControl', []). - directive('contenteditable', function() { + angular.module('customControl', ['ngSanitize']). + directive('contenteditable', ['$sce', function($sce) { return { restrict: 'A', // only activate on element attribute require: '?ngModel', // get a hold of NgModelController @@ -11959,48 +17621,64 @@ var VALID_CLASS = 'ng-valid', // Specify how UI should be updated ngModel.$render = function() { - element.html(ngModel.$viewValue || ''); + element.html($sce.getTrustedHtml(ngModel.$viewValue || '')); }; // Listen for change events to enable binding - element.bind('blur keyup change', function() { - scope.$apply(read); + element.on('blur keyup change', function() { + scope.$evalAsync(read); }); read(); // initialize // Write data to the model function read() { - ngModel.$setViewValue(element.html()); + var html = element.html(); + // When we clear the content editable the browser leaves a
behind + // If strip-br attribute is provided then we strip this out + if( attrs.stripBr && html == '
' ) { + html = ''; + } + ngModel.$setViewValue(html); } } }; - }); + }]);
Change me!
Required!
- - it('should data-bind and become invalid', function() { - var contentEditable = element('[contenteditable]'); - - expect(contentEditable.text()).toEqual('Change me!'); - input('userContent').enter(''); - expect(contentEditable.text()).toEqual(''); - expect(contentEditable.prop('className')).toMatch(/ng-invalid-required/); - }); + + it('should data-bind and become invalid', function() { + if (browser.params.browser == 'safari' || browser.params.browser == 'firefox') { + // SafariDriver can't handle contenteditable + // and Firefox driver can't clear contenteditables very well + return; + } + var contentEditable = element(by.css('[contenteditable]')); + var content = 'Change me!'; + + expect(contentEditable.getText()).toEqual(content); + + contentEditable.clear(); + contentEditable.sendKeys(protractor.Key.BACK_SPACE); + expect(contentEditable.getText()).toEqual(''); + expect(contentEditable.getAttribute('class')).toMatch(/ng-invalid-required/); + }); *
* + * */ -var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', - function($scope, $exceptionHandler, $attr, $element, $parse) { +var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', + function($scope, $exceptionHandler, $attr, $element, $parse, $animate) { this.$viewValue = Number.NaN; this.$modelValue = Number.NaN; this.$parsers = []; @@ -12016,14 +17694,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ ngModelSet = ngModelGet.assign; if (!ngModelSet) { - throw Error(NON_ASSIGNABLE_MODEL_EXPRESSION + $attr.ngModel + - ' (' + startingTag($element) + ')'); + throw minErr('ngModel')('nonassign', "Expression '{0}' is non-assignable. Element: {1}", + $attr.ngModel, startingTag($element)); } /** - * @ngdoc function - * @name ng.directive:ngModel.NgModelController#$render - * @methodOf ng.directive:ngModel.NgModelController + * @ngdoc method + * @name ngModel.NgModelController#$render * * @description * Called when the view needs to be updated. It is expected that the user of the ng-model @@ -12031,6 +17708,27 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ */ this.$render = noop; + /** + * @ngdoc method + * @name ngModel.NgModelController#$isEmpty + * + * @description + * This is called when we need to determine if the value of the input is empty. + * + * For instance, the required directive does this to work out if the input has data or not. + * The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`. + * + * You can override this for input directives whose concept of being empty is different to the + * default. The `checkboxInputType` directive does this because in its case a value of `false` + * implies empty. + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is empty. + */ + this.$isEmpty = function(value) { + return isUndefined(value) || value === '' || value === null || value !== value; + }; + var parentForm = $element.inheritedData('$formController') || nullFormCtrl, invalidCount = 0, // used to easily determine if we are valid $error = this.$error = {}; // keep invalid keys here @@ -12043,15 +17741,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ // convenience method for easy toggling of classes function toggleValidCss(isValid, validationErrorKey) { validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; - $element. - removeClass((isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey). - addClass((isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); + $animate.removeClass($element, (isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey); + $animate.addClass($element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); } /** - * @ngdoc function - * @name ng.directive:ngModel.NgModelController#$setValidity - * @methodOf ng.directive:ngModel.NgModelController + * @ngdoc method + * @name ngModel.NgModelController#$setValidity * * @description * Change the validity state, and notifies the form when the control changes validity. (i.e. it @@ -12060,14 +17756,17 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * This method should be called by validators - i.e. the parser or formatter functions. * * @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign - * to `$error[validationErrorKey]=isValid` so that it is available for data-binding. + * to `$error[validationErrorKey]=!isValid` so that it is available for data-binding. * The `validationErrorKey` should be in camelCase and will get converted into dash-case * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` * class and can be bound to as `{{someForm.someControl.$error.myError}}` . * @param {boolean} isValid Whether the current state is valid (true) or invalid (false). */ this.$setValidity = function(validationErrorKey, isValid) { + // Purposeful use of ! here to cast isValid to boolean in case it is undefined + // jshint -W018 if ($error[validationErrorKey] === !isValid) return; + // jshint +W018 if (isValid) { if ($error[validationErrorKey]) invalidCount--; @@ -12089,21 +17788,41 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ parentForm.$setValidity(validationErrorKey, isValid, this); }; + /** + * @ngdoc method + * @name ngModel.NgModelController#$setPristine + * + * @description + * Sets the control to its pristine state. + * + * This method can be called to remove the 'ng-dirty' class and set the control to its pristine + * state (ng-pristine class). + */ + this.$setPristine = function () { + this.$dirty = false; + this.$pristine = true; + $animate.removeClass($element, DIRTY_CLASS); + $animate.addClass($element, PRISTINE_CLASS); + }; /** - * @ngdoc function - * @name ng.directive:ngModel.NgModelController#$setViewValue - * @methodOf ng.directive:ngModel.NgModelController + * @ngdoc method + * @name ngModel.NgModelController#$setViewValue * * @description - * Read a value from view. + * Update the view value. * - * This method should be called from within a DOM event handler. - * For example {@link ng.directive:input input} or + * This method should be called when the view value changes, typically from within a DOM event handler. + * For example {@link ng.directive:input input} and * {@link ng.directive:select select} directives call it. * - * It internally calls all `parsers` and if resulted value is valid, updates the model and - * calls all registered change listeners. + * It will update the $viewValue, then pass this value through each of the functions in `$parsers`, + * which includes any validators. The value that comes out of this `$parsers` pipeline, be applied to + * `$modelValue` and the **expression** specified in the `ng-model` attribute. + * + * Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called. + * + * Note that calling this function does not trigger a `$digest`. * * @param {string} value Value from the view. */ @@ -12114,7 +17833,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ if (this.$pristine) { this.$dirty = true; this.$pristine = false; - $element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS); + $animate.removeClass($element, PRISTINE_CLASS); + $animate.addClass($element, DIRTY_CLASS); parentForm.$setDirty(); } @@ -12131,7 +17851,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ } catch(e) { $exceptionHandler(e); } - }) + }); } }; @@ -12157,41 +17877,114 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ ctrl.$render(); } } + + return value; }); }]; /** * @ngdoc directive - * @name ng.directive:ngModel + * @name ngModel * * @element input * * @description - * Is directive that tells Angular to do two-way data binding. It works together with `input`, - * `select`, `textarea`. You can easily write your own directives to use `ngModel` as well. + * The `ngModel` directive binds an `input`,`select`, `textarea` (or custom form control) to a + * property on the scope using {@link ngModel.NgModelController NgModelController}, + * which is created and exposed by this directive. * * `ngModel` is responsible for: * - * - binding the view into the model, which other directives such as `input`, `textarea` or `select` - * require, - * - providing validation behavior (i.e. required, number, email, url), - * - keeping state of the control (valid/invalid, dirty/pristine, validation errors), - * - setting related css class onto the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`), - * - register the control with parent {@link ng.directive:form form}. + * - Binding the view into the model, which other directives such as `input`, `textarea` or `select` + * require. + * - Providing validation behavior (i.e. required, number, email, url). + * - Keeping the state of the control (valid/invalid, dirty/pristine, validation errors). + * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`) including animations. + * - Registering the control with its parent {@link ng.directive:form form}. + * + * Note: `ngModel` will try to bind to the property given by evaluating the expression on the + * current scope. If the property doesn't already exist on this scope, it will be created + * implicitly and added to the scope. + * + * For best practices on using `ngModel`, see: + * + * - [Understanding Scopes](https://github.com/angular/angular.js/wiki/Understanding-Scopes) * * For basic examples, how to use `ngModel`, see: * * - {@link ng.directive:input input} - * - {@link ng.directive:input.text text} - * - {@link ng.directive:input.checkbox checkbox} - * - {@link ng.directive:input.radio radio} - * - {@link ng.directive:input.number number} - * - {@link ng.directive:input.email email} - * - {@link ng.directive:input.url url} + * - {@link input[text] text} + * - {@link input[checkbox] checkbox} + * - {@link input[radio] radio} + * - {@link input[number] number} + * - {@link input[email] email} + * - {@link input[url] url} * - {@link ng.directive:select select} * - {@link ng.directive:textarea textarea} * + * # CSS classes + * The following CSS classes are added and removed on the associated input/select/textarea element + * depending on the validity of the model. + * + * - `ng-valid` is set if the model is valid. + * - `ng-invalid` is set if the model is invalid. + * - `ng-pristine` is set if the model is pristine. + * - `ng-dirty` is set if the model is dirty. + * + * Keep in mind that ngAnimate can detect each of these classes when added and removed. + * + * ## Animation Hooks + * + * Animations within models are triggered when any of the associated CSS classes are added and removed + * on the input element which is attached to the model. These classes are: `.ng-pristine`, `.ng-dirty`, + * `.ng-invalid` and `.ng-valid` as well as any other validations that are performed on the model itself. + * The animations that are triggered within ngModel are similar to how they work in ngClass and + * animations can be hooked into using CSS transitions, keyframes as well as JS animations. + * + * The following example shows a simple way to utilize CSS transitions to style an input element + * that has been rendered as invalid after it has been validated: + * + *
+ * //be sure to include ngAnimate as a module to hook into more
+ * //advanced animations
+ * .my-input {
+ *   transition:0.5s linear all;
+ *   background: white;
+ * }
+ * .my-input.ng-invalid {
+ *   background: red;
+ *   color:white;
+ * }
+ * 
+ * + * @example + * + + + + Update input to see transitions when valid/invalid. + Integer is a valid value. +
+ +
+
+ *
*/ var ngModelDirective = function() { return { @@ -12205,7 +17998,7 @@ var ngModelDirective = function() { formCtrl.$addControl(modelCtrl); - element.bind('$destroy', function() { + scope.$on('$destroy', function() { formCtrl.$removeControl(modelCtrl); }); } @@ -12215,51 +18008,62 @@ var ngModelDirective = function() { /** * @ngdoc directive - * @name ng.directive:ngChange - * @restrict E + * @name ngChange * * @description - * Evaluate given expression when user changes the input. + * Evaluate the given expression when the user changes the input. + * The expression is evaluated immediately, unlike the JavaScript onchange event + * which only triggers at the end of a change (usually, when the user leaves the + * form element or presses the return key). * The expression is not evaluated when the value change is coming from the model. * * Note, this directive requires `ngModel` to be present. * * @element input + * @param {expression} ngChange {@link guide/expression Expression} to evaluate upon change + * in input value. * * @example - * - * + * + * * - *
+ *
* * *
- * debug = {{confirmed}}
- * counter = {{counter}} + * debug = {{confirmed}}
+ * counter = {{counter}}
*
- * - * + * + * + * var counter = element(by.binding('counter')); + * var debug = element(by.binding('confirmed')); + * * it('should evaluate the expression if changing from view', function() { - * expect(binding('counter')).toEqual('0'); - * element('#ng-change-example1').click(); - * expect(binding('counter')).toEqual('1'); - * expect(binding('confirmed')).toEqual('true'); + * expect(counter.getText()).toContain('0'); + * + * element(by.id('ng-change-example1')).click(); + * + * expect(counter.getText()).toContain('1'); + * expect(debug.getText()).toContain('true'); * }); * * it('should not evaluate the expression if changing from model', function() { - * element('#ng-change-example2').click(); - * expect(binding('counter')).toEqual('0'); - * expect(binding('confirmed')).toEqual('true'); + * element(by.id('ng-change-example2')).click(); + + * expect(counter.getText()).toContain('0'); + * expect(debug.getText()).toContain('true'); * }); - * - * + * + * */ var ngChangeDirective = valueFn({ require: 'ngModel', @@ -12279,7 +18083,7 @@ var requiredDirective = function() { attr.required = true; // force truthy in case we are on non input element var validator = function(value) { - if (attr.required && (isEmpty(value) || value === false)) { + if (attr.required && ctrl.$isEmpty(value)) { ctrl.$setValidity('required', false); return; } else { @@ -12301,47 +18105,58 @@ var requiredDirective = function() { /** * @ngdoc directive - * @name ng.directive:ngList + * @name ngList * * @description - * Text input that converts between comma-separated string into an array of strings. + * Text input that converts between a delimited string and an array of strings. The delimiter + * can be a fixed string (by default a comma) or a regular expression. * * @element input * @param {string=} ngList optional delimiter that should be used to split the value. If * specified in form `/something/` then the value will be converted into a regular expression. * * @example - - + + -
+ List: - + Required! +
names = {{names}}
myForm.namesInput.$valid = {{myForm.namesInput.$valid}}
myForm.namesInput.$error = {{myForm.namesInput.$error}}
myForm.$valid = {{myForm.$valid}}
myForm.$error.required = {{!!myForm.$error.required}}
-
- + + + var listInput = element(by.model('names')); + var names = element(by.binding('{{names}}')); + var valid = element(by.binding('myForm.namesInput.$valid')); + var error = element(by.css('span.error')); + it('should initialize to model', function() { - expect(binding('names')).toEqual('["igor","misko","vojta"]'); - expect(binding('myForm.namesInput.$valid')).toEqual('true'); + expect(names.getText()).toContain('["igor","misko","vojta"]'); + expect(valid.getText()).toContain('true'); + expect(error.getCssValue('display')).toBe('none'); }); it('should be invalid if empty', function() { - input('names').enter(''); - expect(binding('names')).toEqual('[]'); - expect(binding('myForm.namesInput.$valid')).toEqual('false'); - }); - -
+ listInput.clear(); + listInput.sendKeys(''); + + expect(names.getText()).toContain(''); + expect(valid.getText()).toContain('false'); + expect(error.getCssValue('display')).not.toBe('none'); }); + + */ var ngListDirective = function() { return { @@ -12351,6 +18166,9 @@ var ngListDirective = function() { separator = match && new RegExp(match[1]) || attr.ngList || ','; var parse = function(viewValue) { + // If the viewValue is invalid (say required but empty) it will be `undefined` + if (isUndefined(viewValue)) return; + var list = []; if (viewValue) { @@ -12370,25 +18188,81 @@ var ngListDirective = function() { return undefined; }); + + // Override the standard $isEmpty because an empty array means the input is empty. + ctrl.$isEmpty = function(value) { + return !value || !value.length; + }; } }; }; var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; +/** + * @ngdoc directive + * @name ngValue + * + * @description + * Binds the given expression to the value of `input[select]` or `input[radio]`, so + * that when the element is selected, the `ngModel` of that element is set to the + * bound value. + * + * `ngValue` is useful when dynamically generating lists of radio buttons using `ng-repeat`, as + * shown below. + * + * @element input + * @param {string=} ngValue angular expression, whose value will be bound to the `value` attribute + * of the `input` element + * + * @example + + + +
+

Which is your favorite?

+ +
You chose {{my.favorite}}
+
+
+ + var favorite = element(by.binding('my.favorite')); + it('should initialize to model', function() { + expect(favorite.getText()).toContain('unicorns'); + }); + it('should bind the values to the inputs', function() { + element.all(by.model('my.favorite')).get(0).click(); + expect(favorite.getText()).toContain('pizza'); + }); + +
+ */ var ngValueDirective = function() { return { priority: 100, compile: function(tpl, tplAttr) { if (CONSTANT_VALUE_REGEXP.test(tplAttr.ngValue)) { - return function(scope, elm, attr) { + return function ngValueConstantLink(scope, elm, attr) { attr.$set('value', scope.$eval(attr.ngValue)); }; } else { - return function(scope, elm, attr) { + return function ngValueLink(scope, elm, attr) { scope.$watch(attr.ngValue, function valueWatchAction(value) { - attr.$set('value', value, false); + attr.$set('value', value); }); }; } @@ -12398,7 +18272,8 @@ var ngValueDirective = function() { /** * @ngdoc directive - * @name ng.directive:ngBind + * @name ngBind + * @restrict AC * * @description * The `ngBind` attribute tells Angular to replace the text content of the specified HTML element @@ -12408,10 +18283,9 @@ var ngValueDirective = function() { * Typically, you don't use `ngBind` directly, but instead you use the double curly markup like * `{{ expression }}` which is similar but less verbose. * - * One scenario in which the use of `ngBind` is preferred over `{{ expression }}` binding is when - * it's desirable to put bindings into template that is momentarily displayed by the browser in its - * raw state before Angular compiles it. Since `ngBind` is an element attribute, it makes the - * bindings invisible to the user while the page is loading. + * It is preferable to use `ngBind` instead of `{{ expression }}` if a template is momentarily + * displayed by the browser in its raw state before Angular compiles it. Since `ngBind` is an + * element attribute, it makes the bindings invisible to the user while the page is loading. * * An alternative solution to this problem would be using the * {@link ng.directive:ngCloak ngCloak} directive. @@ -12422,45 +18296,58 @@ var ngValueDirective = function() { * * @example * Enter a name in the Live Preview text box; the greeting below the text box changes instantly. - - + + -
+
Enter name:
Hello !
- - + + it('should check ng-bind', function() { - expect(using('.doc-example-live').binding('name')).toBe('Whirled'); - using('.doc-example-live').input('name').enter('world'); - expect(using('.doc-example-live').binding('name')).toBe('world'); + var nameInput = element(by.model('name')); + + expect(element(by.binding('name')).getText()).toBe('Whirled'); + nameInput.clear(); + nameInput.sendKeys('world'); + expect(element(by.binding('name')).getText()).toBe('world'); }); - - + + */ -var ngBindDirective = ngDirective(function(scope, element, attr) { - element.addClass('ng-binding').data('$binding', attr.ngBind); - scope.$watch(attr.ngBind, function ngBindWatchAction(value) { - element.text(value == undefined ? '' : value); - }); +var ngBindDirective = ngDirective({ + compile: function(templateElement) { + templateElement.addClass('ng-binding'); + return function (scope, element, attr) { + element.data('$binding', attr.ngBind); + scope.$watch(attr.ngBind, function ngBindWatchAction(value) { + // We are purposefully using == here rather than === because we want to + // catch when value is "null or undefined" + // jshint -W041 + element.text(value == undefined ? '' : value); + }); + }; + } }); /** * @ngdoc directive - * @name ng.directive:ngBindTemplate + * @name ngBindTemplate * * @description * The `ngBindTemplate` directive specifies that the element - * text should be replaced with the template in ngBindTemplate. - * Unlike ngBind the ngBindTemplate can contain multiple `{{` `}}` - * expressions. (This is required since some HTML elements - * can not have SPAN elements such as TITLE, or OPTION to name a few.) + * text content should be replaced with the interpolation of the template + * in the `ngBindTemplate` attribute. + * Unlike `ngBind`, the `ngBindTemplate` can contain multiple `{{` `}}` + * expressions. This directive is needed since some HTML elements + * (such as TITLE and OPTION) cannot contain SPAN elements. * * @element ANY * @param {string} ngBindTemplate template of form @@ -12468,35 +18355,38 @@ var ngBindDirective = ngDirective(function(scope, element, attr) { * * @example * Try it here: enter text in text box and watch the greeting change. - - + + -
+
Salutation:
Name:

        
- - + + it('should check ng-bind', function() { - expect(using('.doc-example-live').binding('salutation')). - toBe('Hello'); - expect(using('.doc-example-live').binding('name')). - toBe('World'); - using('.doc-example-live').input('salutation').enter('Greetings'); - using('.doc-example-live').input('name').enter('user'); - expect(using('.doc-example-live').binding('salutation')). - toBe('Greetings'); - expect(using('.doc-example-live').binding('name')). - toBe('user'); + var salutationElem = element(by.binding('salutation')); + var salutationInput = element(by.model('salutation')); + var nameInput = element(by.model('name')); + + expect(salutationElem.getText()).toBe('Hello World!'); + + salutationInput.clear(); + salutationInput.sendKeys('Greetings'); + nameInput.clear(); + nameInput.sendKeys('user'); + + expect(salutationElem.getText()).toBe('Greetings user!'); }); - - + + */ var ngBindTemplateDirective = ['$interpolate', function($interpolate) { return function(scope, element, attr) { @@ -12506,152 +18396,353 @@ var ngBindTemplateDirective = ['$interpolate', function($interpolate) { attr.$observe('ngBindTemplate', function(value) { element.text(value); }); - } + }; }]; /** * @ngdoc directive - * @name ng.directive:ngBindHtmlUnsafe + * @name ngBindHtml * * @description * Creates a binding that will innerHTML the result of evaluating the `expression` into the current - * element. *The innerHTML-ed content will not be sanitized!* You should use this directive only if - * {@link ngSanitize.directive:ngBindHtml ngBindHtml} directive is too - * restrictive and when you absolutely trust the source of the content you are binding to. + * element in a secure way. By default, the innerHTML-ed content will be sanitized using the {@link + * ngSanitize.$sanitize $sanitize} service. To utilize this functionality, ensure that `$sanitize` + * is available, for example, by including {@link ngSanitize} in your module's dependencies (not in + * core Angular). In order to use {@link ngSanitize} in your module's dependencies, you need to + * include "angular-sanitize.js" in your application. * - * See {@link ngSanitize.$sanitize $sanitize} docs for examples. + * You may also bypass sanitization for values you know are safe. To do so, bind to + * an explicitly trusted value via {@link ng.$sce#trustAsHtml $sce.trustAsHtml}. See the example + * under {@link ng.$sce#Example Strict Contextual Escaping (SCE)}. + * + * Note: If a `$sanitize` service is unavailable and the bound value isn't explicitly trusted, you + * will have an exception (instead of an exploit.) * * @element ANY - * @param {expression} ngBindHtmlUnsafe {@link guide/expression Expression} to evaluate. + * @param {expression} ngBindHtml {@link guide/expression Expression} to evaluate. + * + * @example + + + +
+

+
+
+ + + angular.module('bindHtmlExample', ['ngSanitize']) + .controller('ExampleController', ['$scope', function($scope) { + $scope.myHTML = + 'I am an HTMLstring with ' + + 'links! and other stuff'; + }]); + + + + it('should check ng-bind-html', function() { + expect(element(by.binding('myHTML')).getText()).toBe( + 'I am an HTMLstring with links! and other stuff'); + }); + +
*/ -var ngBindHtmlUnsafeDirective = [function() { - return function(scope, element, attr) { - element.addClass('ng-binding').data('$binding', attr.ngBindHtmlUnsafe); - scope.$watch(attr.ngBindHtmlUnsafe, function ngBindHtmlUnsafeWatchAction(value) { - element.html(value || ''); - }); +var ngBindHtmlDirective = ['$sce', '$parse', function($sce, $parse) { + return { + compile: function (tElement) { + tElement.addClass('ng-binding'); + + return function (scope, element, attr) { + element.data('$binding', attr.ngBindHtml); + + var parsed = $parse(attr.ngBindHtml); + + function getStringValue() { + return (parsed(scope) || '').toString(); + } + + scope.$watch(getStringValue, function ngBindHtmlWatchAction(value) { + element.html($sce.getTrustedHtml(parsed(scope)) || ''); + }); + }; + } }; }]; function classDirective(name, selector) { name = 'ngClass' + name; - return ngDirective(function(scope, element, attr) { - var oldVal = undefined; + return ['$animate', function($animate) { + return { + restrict: 'AC', + link: function(scope, element, attr) { + var oldVal; - scope.$watch(attr[name], ngClassWatchAction, true); + scope.$watch(attr[name], ngClassWatchAction, true); - attr.$observe('class', function(value) { - var ngClass = scope.$eval(attr[name]); - ngClassWatchAction(ngClass, ngClass); - }); + attr.$observe('class', function(value) { + ngClassWatchAction(scope.$eval(attr[name])); + }); + + + if (name !== 'ngClass') { + scope.$watch('$index', function($index, old$index) { + // jshint bitwise: false + var mod = $index & 1; + if (mod !== (old$index & 1)) { + var classes = arrayClasses(scope.$eval(attr[name])); + mod === selector ? + addClasses(classes) : + removeClasses(classes); + } + }); + } + + function addClasses(classes) { + var newClasses = digestClassCounts(classes, 1); + attr.$addClass(newClasses); + } + + function removeClasses(classes) { + var newClasses = digestClassCounts(classes, -1); + attr.$removeClass(newClasses); + } + + function digestClassCounts (classes, count) { + var classCounts = element.data('$classCounts') || {}; + var classesToUpdate = []; + forEach(classes, function (className) { + if (count > 0 || classCounts[className]) { + classCounts[className] = (classCounts[className] || 0) + count; + if (classCounts[className] === +(count > 0)) { + classesToUpdate.push(className); + } + } + }); + element.data('$classCounts', classCounts); + return classesToUpdate.join(' '); + } + function updateClasses (oldClasses, newClasses) { + var toAdd = arrayDifference(newClasses, oldClasses); + var toRemove = arrayDifference(oldClasses, newClasses); + toRemove = digestClassCounts(toRemove, -1); + toAdd = digestClassCounts(toAdd, 1); - if (name !== 'ngClass') { - scope.$watch('$index', function($index, old$index) { - var mod = $index & 1; - if (mod !== old$index & 1) { - if (mod === selector) { - addClass(scope.$eval(attr[name])); + if (toAdd.length === 0) { + $animate.removeClass(element, toRemove); + } else if (toRemove.length === 0) { + $animate.addClass(element, toAdd); } else { - removeClass(scope.$eval(attr[name])); + $animate.setClass(element, toAdd, toRemove); } } - }); - } - - function ngClassWatchAction(newVal) { - if (selector === true || scope.$index % 2 === selector) { - if (oldVal && !equals(newVal,oldVal)) { - removeClass(oldVal); + function ngClassWatchAction(newVal) { + if (selector === true || scope.$index % 2 === selector) { + var newClasses = arrayClasses(newVal || []); + if (!oldVal) { + addClasses(newClasses); + } else if (!equals(newVal,oldVal)) { + var oldClasses = arrayClasses(oldVal); + updateClasses(oldClasses, newClasses); + } + } + oldVal = shallowCopy(newVal); } - addClass(newVal); } - oldVal = copy(newVal); - } + }; + function arrayDifference(tokens1, tokens2) { + var values = []; - function removeClass(classVal) { - if (isObject(classVal) && !isArray(classVal)) { - classVal = map(classVal, function(v, k) { if (v) return k }); + outer: + for(var i = 0; i < tokens1.length; i++) { + var token = tokens1[i]; + for(var j = 0; j < tokens2.length; j++) { + if(token == tokens2[j]) continue outer; + } + values.push(token); } - element.removeClass(isArray(classVal) ? classVal.join(' ') : classVal); + return values; } - - function addClass(classVal) { - if (isObject(classVal) && !isArray(classVal)) { - classVal = map(classVal, function(v, k) { if (v) return k }); - } - if (classVal) { - element.addClass(isArray(classVal) ? classVal.join(' ') : classVal); + function arrayClasses (classVal) { + if (isArray(classVal)) { + return classVal; + } else if (isString(classVal)) { + return classVal.split(' '); + } else if (isObject(classVal)) { + var classes = [], i = 0; + forEach(classVal, function(v, k) { + if (v) { + classes = classes.concat(k.split(' ')); + } + }); + return classes; } + return classVal; } - }); + }]; } /** * @ngdoc directive - * @name ng.directive:ngClass + * @name ngClass + * @restrict AC * * @description - * The `ngClass` allows you to set CSS class on HTML element dynamically by databinding an - * expression that represents all classes to be added. + * The `ngClass` directive allows you to dynamically set CSS classes on an HTML element by databinding + * an expression that represents all classes to be added. + * + * The directive operates in three different ways, depending on which of three types the expression + * evaluates to: + * + * 1. If the expression evaluates to a string, the string should be one or more space-delimited class + * names. + * + * 2. If the expression evaluates to an array, each element of the array should be a string that is + * one or more space-delimited class names. + * + * 3. If the expression evaluates to an object, then for each key-value pair of the + * object with a truthy value the corresponding key is used as a class name. * * The directive won't add duplicate classes if a particular class was already set. * * When the expression changes, the previously added classes are removed and only then the * new classes are added. * + * @animations + * add - happens just before the class is applied to the element + * remove - happens just before the class is removed from the element + * * @element ANY * @param {expression} ngClass {@link guide/expression Expression} to eval. The result * of the evaluation can be a string representing space delimited class - * names, an array, or a map of class names to boolean values. + * names, an array, or a map of class names to boolean values. In the case of a map, the + * names of the properties whose values are truthy will be added as css classes to the + * element. * - * @example + * @example Example that demonstrates basic bindings via ngClass directive. - - +

Map Syntax Example

+ deleted (apply "strike" class)
+ important (apply "bold" class)
+ error (apply "red" class) +
+

Using String Syntax

+ +
+

Using Array Syntax

+
+
+
+
+ + .strike { + text-decoration: line-through; + } + .bold { + font-weight: bold; + } + .red { + color: red; + } + + + var ps = element.all(by.css('p')); + + it('should let you toggle the class', function() { + + expect(ps.first().getAttribute('class')).not.toMatch(/bold/); + expect(ps.first().getAttribute('class')).not.toMatch(/red/); + + element(by.model('important')).click(); + expect(ps.first().getAttribute('class')).toMatch(/bold/); + + element(by.model('error')).click(); + expect(ps.first().getAttribute('class')).toMatch(/red/); + }); + + it('should let you toggle string example', function() { + expect(ps.get(1).getAttribute('class')).toBe(''); + element(by.model('style')).clear(); + element(by.model('style')).sendKeys('red'); + expect(ps.get(1).getAttribute('class')).toBe('red'); + }); + + it('array example should have 3 classes', function() { + expect(ps.last().getAttribute('class')).toBe(''); + element(by.model('style1')).sendKeys('bold'); + element(by.model('style2')).sendKeys('strike'); + element(by.model('style3')).sendKeys('red'); + expect(ps.last().getAttribute('class')).toBe('bold strike red'); + }); + +
+ + ## Animations + + The example below demonstrates how to perform animations using ngClass. + + + + +
- Sample Text + Sample Text
- .my-class { + .base-class { + -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + } + + .base-class.my-class { color: red; + font-size:3em; } - + it('should check ng-class', function() { - expect(element('.doc-example-live span').prop('className')).not(). + expect(element(by.css('.base-class')).getAttribute('class')).not. toMatch(/my-class/); - using('.doc-example-live').element(':button:first').click(); + element(by.id('setbtn')).click(); - expect(element('.doc-example-live span').prop('className')). + expect(element(by.css('.base-class')).getAttribute('class')). toMatch(/my-class/); - using('.doc-example-live').element(':button:last').click(); + element(by.id('clearbtn')).click(); - expect(element('.doc-example-live span').prop('className')).not(). + expect(element(by.css('.base-class')).getAttribute('class')).not. toMatch(/my-class/); });
+ + + ## ngClass and pre-existing CSS3 Transitions/Animations + The ngClass directive still supports CSS3 Transitions/Animations even if they do not follow the ngAnimate CSS naming structure. + Upon animation ngAnimate will apply supplementary CSS classes to track the start and end of an animation, but this will not hinder + any pre-existing CSS transitions already on the element. To get an idea of what happens during a class-based animation, be sure + to view the step by step details of {@link ngAnimate.$animate#addclass $animate.addClass} and + {@link ngAnimate.$animate#removeclass $animate.removeClass}. */ var ngClassDirective = classDirective('', true); /** * @ngdoc directive - * @name ng.directive:ngClassOdd + * @name ngClassOdd + * @restrict AC * * @description * The `ngClassOdd` and `ngClassEven` directives work exactly as - * {@link ng.directive:ngClass ngClass}, except it works in - * conjunction with `ngRepeat` and takes affect only on odd (even) rows. + * {@link ng.directive:ngClass ngClass}, except they work in + * conjunction with `ngRepeat` and take effect only on odd (even) rows. * - * This directive can be applied only within a scope of an + * This directive can be applied only within the scope of an * {@link ng.directive:ngRepeat ngRepeat}. * * @element ANY @@ -12677,11 +18768,11 @@ var ngClassDirective = classDirective('', true); color: blue; } - + it('should check ng-class-odd and ng-class-even', function() { - expect(element('.doc-example-live li:first span').prop('className')). + expect(element(by.repeater('name in names').row(0).column('name')).getAttribute('class')). toMatch(/odd/); - expect(element('.doc-example-live li:last span').prop('className')). + expect(element(by.repeater('name in names').row(1).column('name')).getAttribute('class')). toMatch(/even/); }); @@ -12691,14 +18782,15 @@ var ngClassOddDirective = classDirective('Odd', 0); /** * @ngdoc directive - * @name ng.directive:ngClassEven + * @name ngClassEven + * @restrict AC * * @description * The `ngClassOdd` and `ngClassEven` directives work exactly as - * {@link ng.directive:ngClass ngClass}, except it works in - * conjunction with `ngRepeat` and takes affect only on odd (even) rows. + * {@link ng.directive:ngClass ngClass}, except they work in + * conjunction with `ngRepeat` and take effect only on odd (even) rows. * - * This directive can be applied only within a scope of an + * This directive can be applied only within the scope of an * {@link ng.directive:ngRepeat ngRepeat}. * * @element ANY @@ -12724,11 +18816,11 @@ var ngClassOddDirective = classDirective('Odd', 0); color: blue; } - + it('should check ng-class-odd and ng-class-even', function() { - expect(element('.doc-example-live li:first span').prop('className')). + expect(element(by.repeater('name in names').row(0).column('name')).getAttribute('class')). toMatch(/odd/); - expect(element('.doc-example-live li:last span').prop('className')). + expect(element(by.repeater('name in names').row(1).column('name')).getAttribute('class')). toMatch(/even/); }); @@ -12738,55 +18830,58 @@ var ngClassEvenDirective = classDirective('Even', 1); /** * @ngdoc directive - * @name ng.directive:ngCloak + * @name ngCloak + * @restrict AC * * @description * The `ngCloak` directive is used to prevent the Angular html template from being briefly * displayed by the browser in its raw (uncompiled) form while your application is loading. Use this * directive to avoid the undesirable flicker effect caused by the html template display. * - * The directive can be applied to the `` element, but typically a fine-grained application is - * prefered in order to benefit from progressive rendering of the browser view. + * The directive can be applied to the `` element, but the preferred usage is to apply + * multiple `ngCloak` directives to small portions of the page to permit progressive rendering + * of the browser view. * - * `ngCloak` works in cooperation with a css rule that is embedded within `angular.js` and - * `angular.min.js` files. Following is the css rule: + * `ngCloak` works in cooperation with the following css rule embedded within `angular.js` and + * `angular.min.js`. + * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}). * - *
+ * ```css
  * [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
- *   display: none;
+ *   display: none !important;
  * }
- * 
+ * ``` * * When this css rule is loaded by the browser, all html elements (including their children) that - * are tagged with the `ng-cloak` directive are hidden. When Angular comes across this directive - * during the compilation of the template it deletes the `ngCloak` element attribute, which - * makes the compiled element visible. + * are tagged with the `ngCloak` directive are hidden. When Angular encounters this directive + * during the compilation of the template it deletes the `ngCloak` element attribute, making + * the compiled element visible. * - * For the best result, `angular.js` script must be loaded in the head section of the html file; - * alternatively, the css rule (above) must be included in the external stylesheet of the + * For the best result, the `angular.js` script must be loaded in the head section of the html + * document; alternatively, the css rule above must be included in the external stylesheet of the * application. * * Legacy browsers, like IE7, do not provide attribute selector support (added in CSS 2.1) so they * cannot match the `[ng\:cloak]` selector. To work around this limitation, you must add the css - * class `ngCloak` in addition to `ngCloak` directive as shown in the example below. + * class `ng-cloak` in addition to the `ngCloak` directive as shown in the example below. * * @element ANY * * @example - - + +
{{ 'hello' }}
{{ 'hello IE7' }}
-
- +
+ it('should remove the template directive and css class', function() { - expect(element('.doc-example-live #template1').attr('ng-cloak')). - not().toBeDefined(); - expect(element('.doc-example-live #template2').attr('ng-cloak')). - not().toBeDefined(); + expect($('#template1').getAttribute('ng-cloak')). + toBeNull(); + expect($('#template2').getAttribute('ng-cloak')). + toBeNull(); }); - - + + * */ var ngCloakDirective = ngDirective({ @@ -12798,195 +18893,357 @@ var ngCloakDirective = ngDirective({ /** * @ngdoc directive - * @name ng.directive:ngController + * @name ngController * * @description - * The `ngController` directive assigns behavior to a scope. This is a key aspect of how angular + * The `ngController` directive attaches a controller class to the view. This is a key aspect of how angular * supports the principles behind the Model-View-Controller design pattern. * * MVC components in angular: * - * * Model — The Model is data in scope properties; scopes are attached to the DOM. - * * View — The template (HTML with data bindings) is rendered into the View. - * * Controller — The `ngController` directive specifies a Controller class; the class has - * methods that typically express the business logic behind the application. + * * Model — Models are the properties of a scope; scopes are attached to the DOM where scope properties + * are accessed through bindings. + * * View — The template (HTML with data bindings) that is rendered into the View. + * * Controller — The `ngController` directive specifies a Controller class; the class contains business + * logic behind the application to decorate the scope with functions and values * - * Note that an alternative way to define controllers is via the {@link ng.$route $route} service. + * Note that you can also attach controllers to the DOM by declaring it in a route definition + * via the {@link ngRoute.$route $route} service. A common mistake is to declare the controller + * again using `ng-controller` in the template itself. This will cause the controller to be attached + * and executed twice. * * @element ANY * @scope + * @priority 500 * @param {expression} ngController Name of a globally accessible constructor function or an * {@link guide/expression expression} that on the current scope evaluates to a - * constructor function. + * constructor function. The controller instance can be published into a scope property + * by specifying `as propertyName`. * * @example * Here is a simple form for editing user contact information. Adding, removing, clearing, and * greeting are methods declared on the controller (see source tab). These methods can - * easily be called from the angular markup. Notice that the scope becomes the `this` for the - * controller's instance. This allows for easy access to the view data from the controller. Also - * notice that any changes to the data are automatically reflected in the View without the need - * for a manual update. - - - -
- Name: - [ greet ]
- Contact: -
    -
  • - - - [ clear - | X ] -
  • -
  • [ add ]
  • -
-
-
- - it('should check controller', function() { - expect(element('.doc-example-live div>:input').val()).toBe('John Smith'); - expect(element('.doc-example-live li:nth-child(1) input').val()) - .toBe('408 555 1212'); - expect(element('.doc-example-live li:nth-child(2) input').val()) - .toBe('john.smith@example.org'); - - element('.doc-example-live li:first a:contains("clear")').click(); - expect(element('.doc-example-live li:first input').val()).toBe(''); - - element('.doc-example-live li:last a:contains("add")').click(); - expect(element('.doc-example-live li:nth-child(3) input').val()) - .toBe('yourname@example.org'); - }); - -
*/ var ngControllerDirective = [function() { return { scope: true, - controller: '@' + controller: '@', + priority: 500 }; }]; /** * @ngdoc directive - * @name ng.directive:ngCsp - * @priority 1000 + * @name ngCsp * * @element html * @description * Enables [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) support. - * + * * This is necessary when developing things like Google Chrome Extensions. - * + * * CSP forbids apps to use `eval` or `Function(string)` generated functions (among other things). - * For us to be compatible, we just need to implement the "getterFn" in $parse without violating - * any of these restrictions. - * - * AngularJS uses `Function(string)` generated functions as a speed optimization. By applying `ngCsp` - * it is be possible to opt into the CSP compatible mode. When this mode is on AngularJS will + * For Angular to be CSP compatible there are only two things that we need to do differently: + * + * - don't use `Function` constructor to generate optimized value getters + * - don't inject custom stylesheet into the document + * + * AngularJS uses `Function(string)` generated functions as a speed optimization. Applying the `ngCsp` + * directive will cause Angular to use CSP compatibility mode. When this mode is on AngularJS will * evaluate all expressions up to 30% slower than in non-CSP mode, but no security violations will * be raised. - * - * In order to use this feature put `ngCsp` directive on the root element of the application. - * + * + * CSP forbids JavaScript to inline stylesheet rules. In non CSP mode Angular automatically + * includes some CSS rules (e.g. {@link ng.directive:ngCloak ngCloak}). + * To make those directives work in CSP mode, include the `angular-csp.css` manually. + * + * Angular tries to autodetect if CSP is active and automatically turn on the CSP-safe mode. This + * autodetection however triggers a CSP error to be logged in the console: + * + * ``` + * Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of + * script in the following Content Security Policy directive: "default-src 'self'". Note that + * 'script-src' was not explicitly set, so 'default-src' is used as a fallback. + * ``` + * + * This error is harmless but annoying. To prevent the error from showing up, put the `ngCsp` + * directive on the root element of the application or on the `angular.js` script tag, whichever + * appears first in the html document. + * + * *Note: This directive is only available in the `ng-csp` and `data-ng-csp` attribute form.* + * * @example * This example shows how to apply the `ngCsp` directive to the `html` tag. -
+   ```html
      
      
      ...
      ...
      
-   
+ ``` */ -var ngCspDirective = ['$sniffer', function($sniffer) { - return { - priority: 1000, - compile: function() { - $sniffer.csp = true; - } - }; -}]; +// ngCsp is not implemented as a proper directive any more, because we need it be processed while we +// bootstrap the system (before $parse is instantiated), for this reason we just have +// the csp.isActive() fn that looks for ng-csp attribute anywhere in the current doc /** * @ngdoc directive - * @name ng.directive:ngClick + * @name ngClick * * @description - * The ngClick allows you to specify custom behavior when - * element is clicked. + * The ngClick directive allows you to specify custom behavior when + * an element is clicked. * * @element ANY + * @priority 0 * @param {expression} ngClick {@link guide/expression Expression} to evaluate upon - * click. (Event object is available as `$event`) + * click. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - - + + - count: {{count}} - - + + count: {{count}} + + + it('should check ng-click', function() { - expect(binding('count')).toBe('0'); - element('.doc-example-live :button').click(); - expect(binding('count')).toBe('1'); + expect(element(by.binding('count')).getText()).toMatch('0'); + element(by.css('button')).click(); + expect(element(by.binding('count')).getText()).toMatch('1'); }); - - + + */ /* - * A directive that allows creation of custom onclick handlers that are defined as angular - * expressions and are compiled and executed within the current scope. - * - * Events that are handled via these handler are always configured not to propagate further. + * A collection of directives that allows creation of custom event handlers that are defined as + * angular expressions and are compiled and executed within the current scope. */ var ngEventDirectives = {}; + +// For events that might fire synchronously during DOM manipulation +// we need to execute their event handlers asynchronously using $evalAsync, +// so that they are not executed in an inconsistent state. +var forceAsyncEvents = { + 'blur': true, + 'focus': true +}; forEach( - 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave'.split(' '), - function(name) { - var directiveName = directiveNormalize('ng-' + name); - ngEventDirectives[directiveName] = ['$parse', function($parse) { - return function(scope, element, attr) { - var fn = $parse(attr[directiveName]); - element.bind(lowercase(name), function(event) { - scope.$apply(function() { - fn(scope, {$event:event}); - }); - }); + 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '), + function(eventName) { + var directiveName = directiveNormalize('ng-' + eventName); + ngEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse, $rootScope) { + return { + compile: function($element, attr) { + // We expose the powerful $event object on the scope that provides access to the Window, + // etc. that isn't protected by the fast paths in $parse. We explicitly request better + // checks at the cost of speed since event handler expressions are not executed as + // frequently as regular change detection. + var fn = $parse(attr[directiveName], /* expensiveChecks */ true); + return function ngEventHandler(scope, element) { + element.on(eventName, function(event) { + var callback = function() { + fn(scope, {$event:event}); + }; + if (forceAsyncEvents[eventName] && $rootScope.$$phase) { + scope.$evalAsync(callback); + } else { + scope.$apply(callback); + } + }); + }; + } }; }]; } @@ -12994,188 +19251,568 @@ forEach( /** * @ngdoc directive - * @name ng.directive:ngDblclick + * @name ngDblclick * * @description - * The `ngDblclick` directive allows you to specify custom behavior on dblclick event. + * The `ngDblclick` directive allows you to specify custom behavior on a dblclick event. * * @element ANY + * @priority 0 * @param {expression} ngDblclick {@link guide/expression Expression} to evaluate upon - * dblclick. (Event object is available as `$event`) + * a dblclick. (The Event object is available as `$event`) * * @example - * See {@link ng.directive:ngClick ngClick} + + + + count: {{count}} + + */ /** * @ngdoc directive - * @name ng.directive:ngMousedown + * @name ngMousedown * * @description * The ngMousedown directive allows you to specify custom behavior on mousedown event. * * @element ANY + * @priority 0 * @param {expression} ngMousedown {@link guide/expression Expression} to evaluate upon - * mousedown. (Event object is available as `$event`) + * mousedown. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - * See {@link ng.directive:ngClick ngClick} + + + + count: {{count}} + + */ /** * @ngdoc directive - * @name ng.directive:ngMouseup + * @name ngMouseup * * @description * Specify custom behavior on mouseup event. * * @element ANY + * @priority 0 * @param {expression} ngMouseup {@link guide/expression Expression} to evaluate upon - * mouseup. (Event object is available as `$event`) + * mouseup. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - * See {@link ng.directive:ngClick ngClick} + + + + count: {{count}} + + */ /** * @ngdoc directive - * @name ng.directive:ngMouseover + * @name ngMouseover * * @description * Specify custom behavior on mouseover event. * * @element ANY + * @priority 0 * @param {expression} ngMouseover {@link guide/expression Expression} to evaluate upon - * mouseover. (Event object is available as `$event`) + * mouseover. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - * See {@link ng.directive:ngClick ngClick} + + + + count: {{count}} + + */ /** * @ngdoc directive - * @name ng.directive:ngMouseenter + * @name ngMouseenter * * @description * Specify custom behavior on mouseenter event. * * @element ANY + * @priority 0 * @param {expression} ngMouseenter {@link guide/expression Expression} to evaluate upon - * mouseenter. (Event object is available as `$event`) + * mouseenter. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - * See {@link ng.directive:ngClick ngClick} + + + + count: {{count}} + + */ /** * @ngdoc directive - * @name ng.directive:ngMouseleave + * @name ngMouseleave * * @description * Specify custom behavior on mouseleave event. * * @element ANY + * @priority 0 * @param {expression} ngMouseleave {@link guide/expression Expression} to evaluate upon - * mouseleave. (Event object is available as `$event`) + * mouseleave. ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + + + + count: {{count}} + + + */ + + +/** + * @ngdoc directive + * @name ngMousemove + * + * @description + * Specify custom behavior on mousemove event. + * + * @element ANY + * @priority 0 + * @param {expression} ngMousemove {@link guide/expression Expression} to evaluate upon + * mousemove. ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + + + + count: {{count}} + + + */ + + +/** + * @ngdoc directive + * @name ngKeydown + * + * @description + * Specify custom behavior on keydown event. + * + * @element ANY + * @priority 0 + * @param {expression} ngKeydown {@link guide/expression Expression} to evaluate upon + * keydown. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.) + * + * @example + + + + key down count: {{count}} + + + */ + + +/** + * @ngdoc directive + * @name ngKeyup + * + * @description + * Specify custom behavior on keyup event. + * + * @element ANY + * @priority 0 + * @param {expression} ngKeyup {@link guide/expression Expression} to evaluate upon + * keyup. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.) + * + * @example + + +

Typing in the input box below updates the key count

+ key up count: {{count}} + +

Typing in the input box below updates the keycode

+ +

event keyCode: {{ event.keyCode }}

+

event altKey: {{ event.altKey }}

+
+
+ */ + + +/** + * @ngdoc directive + * @name ngKeypress + * + * @description + * Specify custom behavior on keypress event. + * + * @element ANY + * @param {expression} ngKeypress {@link guide/expression Expression} to evaluate upon + * keypress. ({@link guide/expression#-event- Event object is available as `$event`} + * and can be interrogated for keyCode, altKey, etc.) + * + * @example + + + + key press count: {{count}} + + + */ + + +/** + * @ngdoc directive + * @name ngSubmit + * + * @description + * Enables binding angular expressions to onsubmit events. + * + * Additionally it prevents the default action (which for form means sending the request to the + * server and reloading the current page), but only if the form does not contain `action`, + * `data-action`, or `x-action` attributes. + * + *
+ * **Warning:** Be careful not to cause "double-submission" by using both the `ngClick` and + * `ngSubmit` handlers together. See the + * {@link form#submitting-a-form-and-preventing-the-default-action `form` directive documentation} + * for a detailed discussion of when `ngSubmit` may be triggered. + *
+ * + * @element form + * @priority 0 + * @param {expression} ngSubmit {@link guide/expression Expression} to eval. + * ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + + + +
+ Enter text and hit enter: + + +
list={{list}}
+
+
+ + it('should check ng-submit', function() { + expect(element(by.binding('list')).getText()).toBe('list=[]'); + element(by.css('#submit')).click(); + expect(element(by.binding('list')).getText()).toContain('hello'); + expect(element(by.model('text')).getAttribute('value')).toBe(''); + }); + it('should ignore empty strings', function() { + expect(element(by.binding('list')).getText()).toBe('list=[]'); + element(by.css('#submit')).click(); + element(by.css('#submit')).click(); + expect(element(by.binding('list')).getText()).toContain('hello'); + }); + +
+ */ + +/** + * @ngdoc directive + * @name ngFocus + * + * @description + * Specify custom behavior on focus event. + * + * Note: As the `focus` event is executed synchronously when calling `input.focus()` + * AngularJS executes the expression using `scope.$evalAsync` if the event is fired + * during an `$apply` to ensure a consistent state. + * + * @element window, input, select, textarea, a + * @priority 0 + * @param {expression} ngFocus {@link guide/expression Expression} to evaluate upon + * focus. ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + * See {@link ng.directive:ngClick ngClick} + */ + +/** + * @ngdoc directive + * @name ngBlur + * + * @description + * Specify custom behavior on blur event. + * + * A [blur event](https://developer.mozilla.org/en-US/docs/Web/Events/blur) fires when + * an element has lost focus. + * + * Note: As the `blur` event is executed synchronously also during DOM manipulations + * (e.g. removing a focussed input), + * AngularJS executes the expression using `scope.$evalAsync` if the event is fired + * during an `$apply` to ensure a consistent state. + * + * @element window, input, select, textarea, a + * @priority 0 + * @param {expression} ngBlur {@link guide/expression Expression} to evaluate upon + * blur. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example * See {@link ng.directive:ngClick ngClick} */ - /** * @ngdoc directive - * @name ng.directive:ngMousemove + * @name ngCopy + * + * @description + * Specify custom behavior on copy event. + * + * @element window, input, select, textarea, a + * @priority 0 + * @param {expression} ngCopy {@link guide/expression Expression} to evaluate upon + * copy. ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + + + + copied: {{copied}} + + + */ + +/** + * @ngdoc directive + * @name ngCut + * + * @description + * Specify custom behavior on cut event. + * + * @element window, input, select, textarea, a + * @priority 0 + * @param {expression} ngCut {@link guide/expression Expression} to evaluate upon + * cut. ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + + + + cut: {{cut}} + + + */ + +/** + * @ngdoc directive + * @name ngPaste * * @description - * Specify custom behavior on mousemove event. + * Specify custom behavior on paste event. * - * @element ANY - * @param {expression} ngMousemove {@link guide/expression Expression} to evaluate upon - * mousemove. (Event object is available as `$event`) + * @element window, input, select, textarea, a + * @priority 0 + * @param {expression} ngPaste {@link guide/expression Expression} to evaluate upon + * paste. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - * See {@link ng.directive:ngClick ngClick} + + + + pasted: {{paste}} + + */ - /** * @ngdoc directive - * @name ng.directive:ngSubmit + * @name ngIf + * @restrict A * * @description - * Enables binding angular expressions to onsubmit events. + * The `ngIf` directive removes or recreates a portion of the DOM tree based on an + * {expression}. If the expression assigned to `ngIf` evaluates to a false + * value then the element is removed from the DOM, otherwise a clone of the + * element is reinserted into the DOM. + * + * `ngIf` differs from `ngShow` and `ngHide` in that `ngIf` completely removes and recreates the + * element in the DOM rather than changing its visibility via the `display` css property. A common + * case when this difference is significant is when using css selectors that rely on an element's + * position within the DOM, such as the `:first-child` or `:last-child` pseudo-classes. + * + * Note that when an element is removed using `ngIf` its scope is destroyed and a new scope + * is created when the element is restored. The scope created within `ngIf` inherits from + * its parent scope using + * [prototypal inheritance](https://github.com/angular/angular.js/wiki/Understanding-Scopes#javascript-prototypal-inheritance). + * An important implication of this is if `ngModel` is used within `ngIf` to bind to + * a javascript primitive defined in the parent scope. In this case any modifications made to the + * variable within the child scope will override (hide) the value in the parent scope. + * + * Also, `ngIf` recreates elements using their compiled state. An example of this behavior + * is if an element's class attribute is directly modified after it's compiled, using something like + * jQuery's `.addClass()` method, and the element is later removed. When `ngIf` recreates the element + * the added class will be lost because the original compiled state is used to regenerate the element. + * + * Additionally, you can provide animations via the `ngAnimate` module to animate the `enter` + * and `leave` effects. + * + * @animations + * enter - happens just after the `ngIf` contents change and a new DOM element is created and injected into the `ngIf` container + * leave - happens just before the `ngIf` contents are removed from the DOM * - * Additionally it prevents the default action (which for form means sending the request to the - * server and reloading the current page). - * - * @element form - * @param {expression} ngSubmit {@link guide/expression Expression} to eval. + * @element ANY + * @scope + * @priority 600 + * @param {expression} ngIf If the {@link guide/expression expression} is falsy then + * the element is removed from the DOM tree. If it is truthy a copy of the compiled + * element is added to the DOM tree. * * @example - - - -
- Enter text and hit enter: - - -
list={{list}}
-
-
- - it('should check ng-submit', function() { - expect(binding('list')).toBe('[]'); - element('.doc-example-live #submit').click(); - expect(binding('list')).toBe('["hello"]'); - expect(input('text').val()).toBe(''); - }); - it('should ignore empty strings', function() { - expect(binding('list')).toBe('[]'); - element('.doc-example-live #submit').click(); - element('.doc-example-live #submit').click(); - expect(binding('list')).toBe('["hello"]'); - }); - -
+ + + Click me:
+ Show when checked: + + I'm removed when the checkbox is unchecked. + +
+ + .animate-if { + background:white; + border:1px solid black; + padding:10px; + } + + .animate-if.ng-enter, .animate-if.ng-leave { + -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + } + + .animate-if.ng-enter, + .animate-if.ng-leave.ng-leave-active { + opacity:0; + } + + .animate-if.ng-leave, + .animate-if.ng-enter.ng-enter-active { + opacity:1; + } + +
*/ -var ngSubmitDirective = ngDirective(function(scope, element, attrs) { - element.bind('submit', function() { - scope.$apply(attrs.ngSubmit); - }); -}); +var ngIfDirective = ['$animate', function($animate) { + return { + transclude: 'element', + priority: 600, + terminal: true, + restrict: 'A', + $$tlb: true, + link: function ($scope, $element, $attr, ctrl, $transclude) { + var block, childScope, previousElements; + $scope.$watch($attr.ngIf, function ngIfWatchAction(value) { + + if (toBoolean(value)) { + if (!childScope) { + childScope = $scope.$new(); + $transclude(childScope, function (clone) { + clone[clone.length++] = document.createComment(' end ngIf: ' + $attr.ngIf + ' '); + // Note: We only need the first/last node of the cloned nodes. + // However, we need to keep the reference to the jqlite wrapper as it might be changed later + // by a directive with templateUrl when its template arrives. + block = { + clone: clone + }; + $animate.enter(clone, $element.parent(), $element); + }); + } + } else { + if(previousElements) { + previousElements.remove(); + previousElements = null; + } + if(childScope) { + childScope.$destroy(); + childScope = null; + } + if(block) { + previousElements = getBlockElements(block.clone); + $animate.leave(previousElements, function() { + previousElements = null; + }); + block = null; + } + } + }); + } + }; +}]; /** * @ngdoc directive - * @name ng.directive:ngInclude + * @name ngInclude * @restrict ECA * * @description * Fetches, compiles and includes an external HTML fragment. * - * Keep in mind that Same Origin Policy applies to included resources - * (e.g. ngInclude won't work for cross-domain requests on all browsers and for - * file:// access on some browsers). + * By default, the template URL is restricted to the same domain and protocol as the + * application document. This is done by calling {@link ng.$sce#getTrustedResourceUrl + * $sce.getTrustedResourceUrl} on it. To load templates from other domains or protocols + * you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist them} or + * [wrap them](ng.$sce#trustAsResourceUrl) as trusted values. Refer to Angular's {@link + * ng.$sce Strict Contextual Escaping}. + * + * In addition, the browser's + * [Same Origin Policy](https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest) + * and [Cross-Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) + * policy may further restrict whether the template is successfully loaded. + * For example, `ngInclude` won't work for cross-domain requests on all browsers and for `file://` + * access on some browsers. + * + * @animations + * enter - animation is used to bring new content into the browser. + * leave - animation is used to animate existing content away. + * + * The enter and leave animation occur concurrently. * * @scope + * @priority 400 * * @param {string} ngInclude|src angular expression evaluating to URL. If the source is a string constant, - * make sure you wrap it in quotes, e.g. `src="'myPartialTemplate.html'"`. + * make sure you wrap it in **single** quotes, e.g. `src="'myPartialTemplate.html'"`. * @param {string=} onload Expression to evaluate when a new partial is loaded. * * @param {string=} autoscroll Whether `ngInclude` should call {@link ng.$anchorScroll @@ -13186,24 +19823,27 @@ var ngSubmitDirective = ngDirective(function(scope, element, attrs) { * - Otherwise enable scrolling only if the expression evaluates to truthy value. * * @example - + -
+
url of the template: {{template.url}}
-
+
+
+
- function Ctrl($scope) { - $scope.templates = - [ { name: 'template1.html', url: 'template1.html'} - , { name: 'template2.html', url: 'template2.html'} ]; - $scope.template = $scope.templates[0]; - } + angular.module('includeExample', ['ngAnimate']) + .controller('ExampleController', ['$scope', function($scope) { + $scope.templates = + [ { name: 'template1.html', url: 'template1.html'}, + { name: 'template2.html', url: 'template2.html'} ]; + $scope.template = $scope.templates[0]; + }]); Content of template1.html @@ -13211,19 +19851,73 @@ var ngSubmitDirective = ngDirective(function(scope, element, attrs) { Content of template2.html - + + .slide-animate-container { + position:relative; + background:white; + border:1px solid black; + height:40px; + overflow:hidden; + } + + .slide-animate { + padding:10px; + } + + .slide-animate.ng-enter, .slide-animate.ng-leave { + -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + + position:absolute; + top:0; + left:0; + right:0; + bottom:0; + display:block; + padding:10px; + } + + .slide-animate.ng-enter { + top:-50px; + } + .slide-animate.ng-enter.ng-enter-active { + top:0; + } + + .slide-animate.ng-leave { + top:0; + } + .slide-animate.ng-leave.ng-leave-active { + top:50px; + } + + + var templateSelect = element(by.model('template')); + var includeElem = element(by.css('[ng-include]')); + it('should load template1.html', function() { - expect(element('.doc-example-live [ng-include]').text()). - toMatch(/Content of template1.html/); + expect(includeElem.getText()).toMatch(/Content of template1.html/); }); + it('should load template2.html', function() { - select('template').option('1'); - expect(element('.doc-example-live [ng-include]').text()). - toMatch(/Content of template2.html/); + if (browser.params.browser == 'firefox') { + // Firefox can't handle using selects + // See https://github.com/angular/protractor/issues/480 + return; + } + templateSelect.click(); + templateSelect.all(by.css('option')).get(2).click(); + expect(includeElem.getText()).toMatch(/Content of template2.html/); }); + it('should change to blank', function() { - select('template').option(''); - expect(element('.doc-example-live [ng-include]').text()).toEqual(''); + if (browser.params.browser == 'firefox') { + // Firefox can't handle using selects + return; + } + templateSelect.click(); + templateSelect.all(by.css('option')).get(0).click(); + expect(includeElem.isPresent()).toBe(false); }); @@ -13232,155 +19926,242 @@ var ngSubmitDirective = ngDirective(function(scope, element, attrs) { /** * @ngdoc event - * @name ng.directive:ngInclude#$includeContentLoaded - * @eventOf ng.directive:ngInclude + * @name ngInclude#$includeContentRequested + * @eventType emit on the scope ngInclude was declared in + * @description + * Emitted every time the ngInclude content is requested. + */ + + +/** + * @ngdoc event + * @name ngInclude#$includeContentLoaded * @eventType emit on the current ngInclude scope * @description * Emitted every time the ngInclude content is reloaded. */ -var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile', - function($http, $templateCache, $anchorScroll, $compile) { +var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate', '$sce', + function($http, $templateCache, $anchorScroll, $animate, $sce) { return { restrict: 'ECA', + priority: 400, terminal: true, + transclude: 'element', + controller: angular.noop, compile: function(element, attr) { var srcExp = attr.ngInclude || attr.src, onloadExp = attr.onload || '', autoScrollExp = attr.autoscroll; - return function(scope, element) { + return function(scope, $element, $attr, ctrl, $transclude) { var changeCounter = 0, - childScope; - - var clearContent = function() { - if (childScope) { - childScope.$destroy(); - childScope = null; + currentScope, + previousElement, + currentElement; + + var cleanupLastIncludeContent = function() { + if(previousElement) { + previousElement.remove(); + previousElement = null; + } + if(currentScope) { + currentScope.$destroy(); + currentScope = null; + } + if(currentElement) { + $animate.leave(currentElement, function() { + previousElement = null; + }); + previousElement = currentElement; + currentElement = null; } - - element.html(''); }; - scope.$watch(srcExp, function ngIncludeWatchAction(src) { + scope.$watch($sce.parseAsResourceUrl(srcExp), function ngIncludeWatchAction(src) { + var afterAnimation = function() { + if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) { + $anchorScroll(); + } + }; var thisChangeId = ++changeCounter; if (src) { $http.get(src, {cache: $templateCache}).success(function(response) { if (thisChangeId !== changeCounter) return; + var newScope = scope.$new(); + ctrl.template = response; + + // Note: This will also link all children of ng-include that were contained in the original + // html. If that content contains controllers, ... they could pollute/change the scope. + // However, using ng-include on an element with additional content does not make sense... + // Note: We can't remove them in the cloneAttchFn of $transclude as that + // function is called before linking the content, which would apply child + // directives to non existing elements. + var clone = $transclude(newScope, function(clone) { + cleanupLastIncludeContent(); + $animate.enter(clone, null, $element, afterAnimation); + }); - if (childScope) childScope.$destroy(); - childScope = scope.$new(); - - element.html(response); - $compile(element.contents())(childScope); - - if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) { - $anchorScroll(); - } + currentScope = newScope; + currentElement = clone; - childScope.$emit('$includeContentLoaded'); + currentScope.$emit('$includeContentLoaded'); scope.$eval(onloadExp); }).error(function() { - if (thisChangeId === changeCounter) clearContent(); + if (thisChangeId === changeCounter) cleanupLastIncludeContent(); }); - } else clearContent(); + scope.$emit('$includeContentRequested'); + } else { + cleanupLastIncludeContent(); + ctrl.template = null; + } }); }; } }; }]; +// This directive is called during the $transclude call of the first `ngInclude` directive. +// It will replace and compile the content of the element with the loaded template. +// We need this directive so that the element content is already filled when +// the link function of another directive on the same element as ngInclude +// is called. +var ngIncludeFillContentDirective = ['$compile', + function($compile) { + return { + restrict: 'ECA', + priority: -400, + require: 'ngInclude', + link: function(scope, $element, $attr, ctrl) { + $element.html(ctrl.template); + $compile($element.contents())(scope); + } + }; + }]; + /** * @ngdoc directive - * @name ng.directive:ngInit + * @name ngInit + * @restrict AC * * @description - * The `ngInit` directive specifies initialization tasks to be executed - * before the template enters execution mode during bootstrap. + * The `ngInit` directive allows you to evaluate an expression in the + * current scope. + * + *
+ * The only appropriate use of `ngInit` is for aliasing special properties of + * {@link ng.directive:ngRepeat `ngRepeat`}, as seen in the demo below. Besides this case, you + * should use {@link guide/controller controllers} rather than `ngInit` + * to initialize values on a scope. + *
+ *
+ * **Note**: If you have assignment in `ngInit` along with {@link ng.$filter `$filter`}, make + * sure you have parenthesis for correct precedence: + *
+ *   
+ *
+ *
+ * + * @priority 450 * * @element ANY * @param {expression} ngInit {@link guide/expression Expression} to eval. * * @example - - -
- {{greeting}} {{person}}! -
-
- - it('should check greeting', function() { - expect(binding('greeting')).toBe('Hello'); - expect(binding('person')).toBe('World'); + + + +
+
+
+ list[ {{outerIndex}} ][ {{innerIndex}} ] = {{value}}; +
+
+
+
+ + it('should alias index positions', function() { + var elements = element.all(by.css('.example-init')); + expect(elements.get(0).getText()).toBe('list[ 0 ][ 0 ] = a;'); + expect(elements.get(1).getText()).toBe('list[ 0 ][ 1 ] = b;'); + expect(elements.get(2).getText()).toBe('list[ 1 ][ 0 ] = c;'); + expect(elements.get(3).getText()).toBe('list[ 1 ][ 1 ] = d;'); }); -
-
+
+ */ var ngInitDirective = ngDirective({ + priority: 450, compile: function() { return { pre: function(scope, element, attrs) { scope.$eval(attrs.ngInit); } - } + }; } }); /** * @ngdoc directive - * @name ng.directive:ngNonBindable + * @name ngNonBindable + * @restrict AC * @priority 1000 * * @description - * Sometimes it is necessary to write code which looks like bindings but which should be left alone - * by angular. Use `ngNonBindable` to make angular ignore a chunk of HTML. + * The `ngNonBindable` directive tells Angular not to compile or bind the contents of the current + * DOM element. This is useful if the element contains what appears to be Angular directives and + * bindings but which should be ignored by Angular. This could be the case if you have a site that + * displays snippets of code, for instance. * * @element ANY * * @example - * In this example there are two location where a simple binding (`{{}}`) is present, but the one - * wrapped in `ngNonBindable` is left alone. + * In this example there are two locations where a simple interpolation binding (`{{}}`) is present, + * but the one wrapped in `ngNonBindable` is left alone. * * @example - - + +
Normal: {{1 + 2}}
Ignored: {{1 + 2}}
-
- +
+ it('should check ng-non-bindable', function() { - expect(using('.doc-example-live').binding('1 + 2')).toBe('3'); - expect(using('.doc-example-live').element('div:last').text()). - toMatch(/1 \+ 2/); + expect(element(by.binding('1 + 2')).getText()).toContain('3'); + expect(element.all(by.css('div')).last().getText()).toMatch(/1 \+ 2/); }); - - + + */ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); /** * @ngdoc directive - * @name ng.directive:ngPluralize + * @name ngPluralize * @restrict EA * * @description - * # Overview * `ngPluralize` is a directive that displays messages according to en-US localization rules. - * These rules are bundled with angular.js and the rules can be overridden + * These rules are bundled with angular.js, but can be overridden * (see {@link guide/i18n Angular i18n} dev guide). You configure ngPluralize directive * by specifying the mappings between - * {@link http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html - * plural categories} and the strings to be displayed. + * [plural categories](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html) + * and the strings to be displayed. * * # Plural categories and explicit number rules * There are two - * {@link http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html - * plural categories} in Angular's default en-US locale: "one" and "other". + * [plural categories](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html) + * in Angular's default en-US locale: "one" and "other". * - * While a pural category may match many numbers (for example, in en-US locale, "other" can match + * While a plural category may match many numbers (for example, in en-US locale, "other" can match * any number that is not 1), an explicit number rule can only match one number. For example, the - * explicit number rule for "3" matches the number 3. You will see the use of plural categories - * and explicit number rules throughout later parts of this documentation. + * explicit number rule for "3" matches the number 3. There are examples of plural categories + * and explicit number rules throughout the rest of this documentation. * * # Configuring ngPluralize * You configure ngPluralize by providing 2 attributes: `count` and `when`. @@ -13390,18 +20171,17 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); * Angular expression}; these are evaluated on the current scope for its bound value. * * The `when` attribute specifies the mappings between plural categories and the actual - * string to be displayed. The value of the attribute should be a JSON object so that Angular - * can interpret it correctly. + * string to be displayed. The value of the attribute should be a JSON object. * * The following example shows how to configure ngPluralize: * - *
+ * ```html
  * 
  * 
- *
+ *``` * * In the example, `"0: Nobody is viewing."` is an explicit number rule. If you did not * specify this rule, 0 would be matched to the "other" category and "0 people are viewing" @@ -13409,7 +20189,7 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); * other numbers, for example 12, so that instead of showing "12 people are viewing", you can * show "a dozen people are viewing". * - * You can use a set of closed braces(`{}`) as a placeholder for the number that you want substituted + * You can use a set of closed braces (`{}`) as a placeholder for the number that you want substituted * into pluralized strings. In the previous example, Angular will replace `{}` with * `{{personCount}}`. The closed braces `{}` is a placeholder * for {{numberExpression}}. @@ -13421,7 +20201,7 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); * The offset attribute allows you to offset a number by any desired value. * Let's take a look at an example: * - *
+ * ```html
  * 
  * 
- * 
+ * ``` * * Notice that we are still using two plural categories(one, other), but we added * three explicit number rules 0, 1 and 2. * When one person, perhaps John, views the document, "John is viewing" will be shown. * When three people view the document, no explicit number rule is found, so * an offset of 2 is taken off 3, and Angular uses 1 to decide the plural category. - * In this case, plural category 'one' is matched and "John, Marry and one other person are viewing" + * In this case, plural category 'one' is matched and "John, Mary and one other person are viewing" * is shown. * * Note that when you specify offsets, you must provide explicit number rules for @@ -13444,21 +20224,22 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); * you must provide explicit number rules for 0, 1, 2 and 3. You must also provide plural strings for * plural categories "one" and "other". * - * @param {string|expression} count The variable to be bounded to. - * @param {string} when The mapping between plural category to its correspoding strings. + * @param {string|expression} count The variable to be bound to. + * @param {string} when The mapping between plural category to its corresponding strings. * @param {number=} offset Offset to deduct from the total number. * * @example - - + + -
+
Person 1:
Person 2:
Number of People:
@@ -13481,51 +20262,55 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); 'other': '{{person1}}, {{person2}} and {} other people are viewing.'}">
- - + + it('should show correct pluralized string', function() { - expect(element('.doc-example-live ng-pluralize:first').text()). - toBe('1 person is viewing.'); - expect(element('.doc-example-live ng-pluralize:last').text()). - toBe('Igor is viewing.'); - - using('.doc-example-live').input('personCount').enter('0'); - expect(element('.doc-example-live ng-pluralize:first').text()). - toBe('Nobody is viewing.'); - expect(element('.doc-example-live ng-pluralize:last').text()). - toBe('Nobody is viewing.'); - - using('.doc-example-live').input('personCount').enter('2'); - expect(element('.doc-example-live ng-pluralize:first').text()). - toBe('2 people are viewing.'); - expect(element('.doc-example-live ng-pluralize:last').text()). - toBe('Igor and Misko are viewing.'); - - using('.doc-example-live').input('personCount').enter('3'); - expect(element('.doc-example-live ng-pluralize:first').text()). - toBe('3 people are viewing.'); - expect(element('.doc-example-live ng-pluralize:last').text()). - toBe('Igor, Misko and one other person are viewing.'); - - using('.doc-example-live').input('personCount').enter('4'); - expect(element('.doc-example-live ng-pluralize:first').text()). - toBe('4 people are viewing.'); - expect(element('.doc-example-live ng-pluralize:last').text()). - toBe('Igor, Misko and 2 other people are viewing.'); - }); + var withoutOffset = element.all(by.css('ng-pluralize')).get(0); + var withOffset = element.all(by.css('ng-pluralize')).get(1); + var countInput = element(by.model('personCount')); + + expect(withoutOffset.getText()).toEqual('1 person is viewing.'); + expect(withOffset.getText()).toEqual('Igor is viewing.'); + + countInput.clear(); + countInput.sendKeys('0'); + + expect(withoutOffset.getText()).toEqual('Nobody is viewing.'); + expect(withOffset.getText()).toEqual('Nobody is viewing.'); + + countInput.clear(); + countInput.sendKeys('2'); + + expect(withoutOffset.getText()).toEqual('2 people are viewing.'); + expect(withOffset.getText()).toEqual('Igor and Misko are viewing.'); - it('should show data-binded names', function() { - using('.doc-example-live').input('personCount').enter('4'); - expect(element('.doc-example-live ng-pluralize:last').text()). - toBe('Igor, Misko and 2 other people are viewing.'); + countInput.clear(); + countInput.sendKeys('3'); - using('.doc-example-live').input('person1').enter('Di'); - using('.doc-example-live').input('person2').enter('Vojta'); - expect(element('.doc-example-live ng-pluralize:last').text()). - toBe('Di, Vojta and 2 other people are viewing.'); + expect(withoutOffset.getText()).toEqual('3 people are viewing.'); + expect(withOffset.getText()).toEqual('Igor, Misko and one other person are viewing.'); + + countInput.clear(); + countInput.sendKeys('4'); + + expect(withoutOffset.getText()).toEqual('4 people are viewing.'); + expect(withOffset.getText()).toEqual('Igor, Misko and 2 other people are viewing.'); }); - - + it('should show data-bound names', function() { + var withOffset = element.all(by.css('ng-pluralize')).get(1); + var personCount = element(by.model('personCount')); + var person1 = element(by.model('person1')); + var person2 = element(by.model('person2')); + personCount.clear(); + personCount.sendKeys('4'); + person1.clear(); + person1.sendKeys('Di'); + person2.clear(); + person2.sendKeys('Vojta'); + expect(withOffset.getText()).toEqual('Di, Vojta and 2 other people are viewing.'); + }); + + */ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interpolate) { var BRACE = /{}/g; @@ -13533,13 +20318,20 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp restrict: 'EA', link: function(scope, element, attr) { var numberExp = attr.count, - whenExp = element.attr(attr.$attr.when), // this is because we have {{}} in attrs + whenExp = attr.$attr.when && element.attr(attr.$attr.when), // we have {{}} in attrs offset = attr.offset || 0, - whens = scope.$eval(whenExp), + whens = scope.$eval(whenExp) || {}, whensExpFns = {}, startSymbol = $interpolate.startSymbol(), - endSymbol = $interpolate.endSymbol(); + endSymbol = $interpolate.endSymbol(), + isWhen = /^when(Minus)?(.+)$/; + forEach(attr, function(expression, attributeName) { + if (isWhen.test(attributeName)) { + whens[lowercase(attributeName.replace('when', '').replace('Minus', '-'))] = + element.attr(attr.$attr[attributeName]); + } + }); forEach(whens, function(expression, key) { whensExpFns[key] = $interpolate(expression.replace(BRACE, startSymbol + numberExp + '-' + @@ -13566,7 +20358,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp /** * @ngdoc directive - * @name ng.directive:ngRepeat + * @name ngRepeat * * @description * The `ngRepeat` directive instantiates a template once per item from a collection. Each template @@ -13575,278 +20367,726 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp * * Special properties are exposed on the local scope of each template instance, including: * - * * `$index` – `{number}` – iterator offset of the repeated element (0..length-1) - * * `$first` – `{boolean}` – true if the repeated element is first in the iterator. - * * `$middle` – `{boolean}` – true if the repeated element is between the first and last in the iterator. - * * `$last` – `{boolean}` – true if the repeated element is last in the iterator. - * + * | Variable | Type | Details | + * |-----------|-----------------|-----------------------------------------------------------------------------| + * | `$index` | {@type number} | iterator offset of the repeated element (0..length-1) | + * | `$first` | {@type boolean} | true if the repeated element is first in the iterator. | + * | `$middle` | {@type boolean} | true if the repeated element is between the first and last in the iterator. | + * | `$last` | {@type boolean} | true if the repeated element is last in the iterator. | + * | `$even` | {@type boolean} | true if the iterator position `$index` is even (otherwise false). | + * | `$odd` | {@type boolean} | true if the iterator position `$index` is odd (otherwise false). | + * + * Creating aliases for these properties is possible with {@link ng.directive:ngInit `ngInit`}. + * This may be useful when, for instance, nesting ngRepeats. + * + * # Special repeat start and end points + * To repeat a series of elements instead of just one parent element, ngRepeat (as well as other ng directives) supports extending + * the range of the repeater by defining explicit start and end points by using **ng-repeat-start** and **ng-repeat-end** respectively. + * The **ng-repeat-start** directive works the same as **ng-repeat**, but will repeat all the HTML code (including the tag it's defined on) + * up to and including the ending HTML tag where **ng-repeat-end** is placed. + * + * The example below makes use of this feature: + * ```html + *
+ * Header {{ item }} + *
+ *
+ * Body {{ item }} + *
+ *
+ * Footer {{ item }} + *
+ * ``` + * + * And with an input of {@type ['A','B']} for the items variable in the example above, the output will evaluate to: + * ```html + *
+ * Header A + *
+ *
+ * Body A + *
+ *
+ * Footer A + *
+ *
+ * Header B + *
+ *
+ * Body B + *
+ *
+ * Footer B + *
+ * ``` + * + * The custom start and end points for ngRepeat also support all other HTML directive syntax flavors provided in AngularJS (such + * as **data-ng-repeat-start**, **x-ng-repeat-start** and **ng:repeat-start**). + * + * @animations + * **.enter** - when a new item is added to the list or when an item is revealed after a filter + * + * **.leave** - when an item is removed from the list or when an item is filtered out + * + * **.move** - when an adjacent item is filtered out causing a reorder or when the item contents are reordered * * @element ANY * @scope * @priority 1000 - * @param {repeat_expression} ngRepeat The expression indicating how to enumerate a collection. Two + * @param {repeat_expression} ngRepeat The expression indicating how to enumerate a collection. These * formats are currently supported: * * * `variable in expression` – where variable is the user defined loop variable and `expression` * is a scope expression giving the collection to enumerate. * - * For example: `track in cd.tracks`. + * For example: `album in artist.albums`. * * * `(key, value) in expression` – where `key` and `value` can be any user defined identifiers, * and `expression` is the scope expression giving the collection to enumerate. * * For example: `(name, age) in {'adam':10, 'amalie':12}`. * + * * `variable in expression track by tracking_expression` – You can also provide an optional tracking function + * which can be used to associate the objects in the collection with the DOM elements. If no tracking function + * is specified the ng-repeat associates elements by identity in the collection. It is an error to have + * more than one tracking function to resolve to the same key. (This would mean that two distinct objects are + * mapped to the same DOM element, which is not possible.) Filters should be applied to the expression, + * before specifying a tracking expression. + * + * For example: `item in items` is equivalent to `item in items track by $id(item)`. This implies that the DOM elements + * will be associated by item identity in the array. + * + * For example: `item in items track by $id(item)`. A built in `$id()` function can be used to assign a unique + * `$$hashKey` property to each item in the array. This property is then used as a key to associated DOM elements + * with the corresponding item in the array by identity. Moving the same object in array would move the DOM + * element in the same way in the DOM. + * + * For example: `item in items track by item.id` is a typical pattern when the items come from the database. In this + * case the object identity does not matter. Two objects are considered equivalent as long as their `id` + * property is same. + * + * For example: `item in items | filter:searchText track by item.id` is a pattern that might be used to apply a filter + * to items in conjunction with a tracking expression. + * * @example * This example initializes the scope to a list of names and * then uses `ngRepeat` to display every person: - - -
- I have {{friends.length}} friends. They are: -
    -
  • - [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old. -
  • -
-
-
- - it('should check ng-repeat', function() { - var r = using('.doc-example-live').repeater('ul li'); - expect(r.count()).toBe(2); - expect(r.row(0)).toEqual(["1","John","25"]); - expect(r.row(1)).toEqual(["2","Mary","28"]); - }); - -
- */ -var ngRepeatDirective = ngDirective({ - transclude: 'element', - priority: 1000, - terminal: true, - compile: function(element, attr, linker) { - return function(scope, iterStartElement, attr){ - var expression = attr.ngRepeat; - var match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/), - lhs, rhs, valueIdent, keyIdent; - if (! match) { - throw Error("Expected ngRepeat in form of '_item_ in _collection_' but got '" + - expression + "'."); - } - lhs = match[1]; - rhs = match[2]; - match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/); - if (!match) { - throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '" + - lhs + "'."); - } - valueIdent = match[3] || match[1]; - keyIdent = match[2]; - - // Store a list of elements from previous run. This is a hash where key is the item from the - // iterator, and the value is an array of objects with following properties. - // - scope: bound scope - // - element: previous element. - // - index: position - // We need an array of these objects since the same object can be returned from the iterator. - // We expect this to be a rare case. - var lastOrder = new HashQueueMap(); - - scope.$watch(function ngRepeatWatch(scope){ - var index, length, - collection = scope.$eval(rhs), - cursor = iterStartElement, // current position of the node - // Same as lastOrder but it has the current state. It will become the - // lastOrder on the next iteration. - nextOrder = new HashQueueMap(), - arrayBound, - childScope, - key, value, // key/value of iteration - array, - last; // last object information {scope, element, index} - - - - if (!isArray(collection)) { - // if object, extract keys, sort them and use to determine order of iteration over obj props - array = []; - for(key in collection) { - if (collection.hasOwnProperty(key) && key.charAt(0) != '$') { - array.push(key); - } - } - array.sort(); - } else { - array = collection || []; - } + + +
+ I have {{friends.length}} friends. They are: + +
    +
  • + [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old. +
  • +
+
+
+ + .example-animate-container { + background:white; + border:1px solid black; + list-style:none; + margin:0; + padding:0 10px; + } + + .animate-repeat { + line-height:40px; + list-style:none; + box-sizing:border-box; + } + + .animate-repeat.ng-move, + .animate-repeat.ng-enter, + .animate-repeat.ng-leave { + -webkit-transition:all linear 0.5s; + transition:all linear 0.5s; + } - arrayBound = array.length-1; + .animate-repeat.ng-leave.ng-leave-active, + .animate-repeat.ng-move, + .animate-repeat.ng-enter { + opacity:0; + max-height:0; + } - // we are not using forEach for perf reasons (trying to avoid #call) - for (index = 0, length = array.length; index < length; index++) { - key = (collection === array) ? index : array[index]; - value = collection[key]; + .animate-repeat.ng-leave, + .animate-repeat.ng-move.ng-move-active, + .animate-repeat.ng-enter.ng-enter-active { + opacity:1; + max-height:40px; + } + + + var friends = element.all(by.repeater('friend in friends')); + + it('should render initial data set', function() { + expect(friends.count()).toBe(10); + expect(friends.get(0).getText()).toEqual('[1] John who is 25 years old.'); + expect(friends.get(1).getText()).toEqual('[2] Jessie who is 30 years old.'); + expect(friends.last().getText()).toEqual('[10] Samantha who is 60 years old.'); + expect(element(by.binding('friends.length')).getText()) + .toMatch("I have 10 friends. They are:"); + }); - last = lastOrder.shift(value); + it('should update repeater when filter predicate changes', function() { + expect(friends.count()).toBe(10); - if (last) { - // if we have already seen this object, then we need to reuse the - // associated scope/element - childScope = last.scope; - nextOrder.push(value, last); + element(by.model('q')).sendKeys('ma'); - if (index === last.index) { - // do nothing - cursor = last.element; - } else { - // existing item which got moved - last.index = index; - // This may be a noop, if the element is next, but I don't know of a good way to - // figure this out, since it would require extra DOM access, so let's just hope that - // the browsers realizes that it is noop, and treats it as such. - cursor.after(last.element); - cursor = last.element; - } + expect(friends.count()).toBe(2); + expect(friends.get(0).getText()).toEqual('[1] Mary who is 28 years old.'); + expect(friends.last().getText()).toEqual('[2] Samantha who is 60 years old.'); + }); + +
+ */ +var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { + var NG_REMOVED = '$$NG_REMOVED'; + var ngRepeatMinErr = minErr('ngRepeat'); + return { + transclude: 'element', + priority: 1000, + terminal: true, + $$tlb: true, + link: function($scope, $element, $attr, ctrl, $transclude){ + var expression = $attr.ngRepeat; + var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/), + trackByExp, trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn, + lhs, rhs, valueIdentifier, keyIdentifier, + hashFnLocals = {$id: hashKey}; + + if (!match) { + throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.", + expression); + } + + lhs = match[1]; + rhs = match[2]; + trackByExp = match[3]; + + if (trackByExp) { + trackByExpGetter = $parse(trackByExp); + trackByIdExpFn = function(key, value, index) { + // assign key, value, and $index to the locals so that they can be used in hash functions + if (keyIdentifier) hashFnLocals[keyIdentifier] = key; + hashFnLocals[valueIdentifier] = value; + hashFnLocals.$index = index; + return trackByExpGetter($scope, hashFnLocals); + }; + } else { + trackByIdArrayFn = function(key, value) { + return hashKey(value); + }; + trackByIdObjFn = function(key) { + return key; + }; + } + + match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/); + if (!match) { + throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.", + lhs); + } + valueIdentifier = match[3] || match[1]; + keyIdentifier = match[2]; + + // Store a list of elements from previous run. This is a hash where key is the item from the + // iterator, and the value is objects with following properties. + // - scope: bound scope + // - element: previous element. + // - index: position + var lastBlockMap = {}; + + //watch props + $scope.$watchCollection(rhs, function ngRepeatAction(collection){ + var index, length, + previousNode = $element[0], // current position of the node + nextNode, + // Same as lastBlockMap but it has the current state. It will become the + // lastBlockMap on the next iteration. + nextBlockMap = {}, + arrayLength, + childScope, + key, value, // key/value of iteration + trackById, + trackByIdFn, + collectionKeys, + block, // last object information {scope, element, id} + nextBlockOrder = [], + elementsToRemove; + + + if (isArrayLike(collection)) { + collectionKeys = collection; + trackByIdFn = trackByIdExpFn || trackByIdArrayFn; } else { - // new item which we don't know about - childScope = scope.$new(); + trackByIdFn = trackByIdExpFn || trackByIdObjFn; + // if object, extract keys, sort them and use to determine order of iteration over obj props + collectionKeys = []; + for (key in collection) { + if (collection.hasOwnProperty(key) && key.charAt(0) != '$') { + collectionKeys.push(key); + } + } + collectionKeys.sort(); } - childScope[valueIdent] = value; - if (keyIdent) childScope[keyIdent] = key; - childScope.$index = index; - - childScope.$first = (index === 0); - childScope.$last = (index === arrayBound); - childScope.$middle = !(childScope.$first || childScope.$last); - - if (!last) { - linker(childScope, function(clone){ - cursor.after(clone); - last = { - scope: childScope, - element: (cursor = clone), - index: index - }; - nextOrder.push(value, last); - }); + arrayLength = collectionKeys.length; + + // locate existing items + length = nextBlockOrder.length = collectionKeys.length; + for(index = 0; index < length; index++) { + key = (collection === collectionKeys) ? index : collectionKeys[index]; + value = collection[key]; + trackById = trackByIdFn(key, value, index); + assertNotHasOwnProperty(trackById, '`track by` id'); + if(lastBlockMap.hasOwnProperty(trackById)) { + block = lastBlockMap[trackById]; + delete lastBlockMap[trackById]; + nextBlockMap[trackById] = block; + nextBlockOrder[index] = block; + } else if (nextBlockMap.hasOwnProperty(trackById)) { + // restore lastBlockMap + forEach(nextBlockOrder, function(block) { + if (block && block.scope) lastBlockMap[block.id] = block; + }); + // This is a duplicate and we need to throw an error + throw ngRepeatMinErr('dupes', + "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}", + expression, trackById, toJson(value)); + } else { + // new never before seen block + nextBlockOrder[index] = { id: trackById }; + nextBlockMap[trackById] = false; + } + } + + // remove existing items + for (key in lastBlockMap) { + // lastBlockMap is our own object so we don't need to use special hasOwnPropertyFn + if (lastBlockMap.hasOwnProperty(key)) { + block = lastBlockMap[key]; + elementsToRemove = getBlockElements(block.clone); + $animate.leave(elementsToRemove); + forEach(elementsToRemove, function(element) { element[NG_REMOVED] = true; }); + block.scope.$destroy(); + } } - } - //shrink children - for (key in lastOrder) { - if (lastOrder.hasOwnProperty(key)) { - array = lastOrder[key]; - while(array.length) { - value = array.pop(); - value.element.remove(); - value.scope.$destroy(); + // we are not using forEach for perf reasons (trying to avoid #call) + for (index = 0, length = collectionKeys.length; index < length; index++) { + key = (collection === collectionKeys) ? index : collectionKeys[index]; + value = collection[key]; + block = nextBlockOrder[index]; + if (nextBlockOrder[index - 1]) previousNode = getBlockEnd(nextBlockOrder[index - 1]); + + if (block.scope) { + // if we have already seen this object, then we need to reuse the + // associated scope/element + childScope = block.scope; + + nextNode = previousNode; + do { + nextNode = nextNode.nextSibling; + } while(nextNode && nextNode[NG_REMOVED]); + + if (getBlockStart(block) != nextNode) { + // existing item which got moved + $animate.move(getBlockElements(block.clone), null, jqLite(previousNode)); + } + previousNode = getBlockEnd(block); + } else { + // new item which we don't know about + childScope = $scope.$new(); + } + + childScope[valueIdentifier] = value; + if (keyIdentifier) childScope[keyIdentifier] = key; + childScope.$index = index; + childScope.$first = (index === 0); + childScope.$last = (index === (arrayLength - 1)); + childScope.$middle = !(childScope.$first || childScope.$last); + // jshint bitwise: false + childScope.$odd = !(childScope.$even = (index&1) === 0); + // jshint bitwise: true + + if (!block.scope) { + $transclude(childScope, function(clone) { + clone[clone.length++] = document.createComment(' end ngRepeat: ' + expression + ' '); + $animate.enter(clone, null, jqLite(previousNode)); + previousNode = clone; + block.scope = childScope; + // Note: We only need the first/last node of the cloned nodes. + // However, we need to keep the reference to the jqlite wrapper as it might be changed later + // by a directive with templateUrl when its template arrives. + block.clone = clone; + nextBlockMap[block.id] = block; + }); } } - } + lastBlockMap = nextBlockMap; + }); + } + }; - lastOrder = nextOrder; - }); - }; + function getBlockStart(block) { + return block.clone[0]; } -}); + + function getBlockEnd(block) { + return block.clone[block.clone.length - 1]; + } +}]; /** * @ngdoc directive - * @name ng.directive:ngShow + * @name ngShow * * @description - * The `ngShow` and `ngHide` directives show or hide a portion of the DOM tree (HTML) - * conditionally. + * The `ngShow` directive shows or hides the given HTML element based on the expression + * provided to the `ngShow` attribute. The element is shown or hidden by removing or adding + * the `.ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined + * in AngularJS and sets the display style to none (using an !important flag). + * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}). + * + * ```html + * + *
+ * + * + *
+ * ``` + * + * When the `ngShow` expression evaluates to false then the `.ng-hide` CSS class is added to the class attribute + * on the element causing it to become hidden. When true, the `.ng-hide` CSS class is removed + * from the element causing the element not to appear hidden. + * + *
+ * **Note:** Here is a list of values that ngShow will consider as a falsy value (case insensitive):
+ * "f" / "0" / "false" / "no" / "n" / "[]" + *
+ * + * ## Why is !important used? + * + * You may be wondering why !important is used for the `.ng-hide` CSS class. This is because the `.ng-hide` selector + * can be easily overridden by heavier selectors. For example, something as simple + * as changing the display style on a HTML list item would make hidden elements appear visible. + * This also becomes a bigger issue when dealing with CSS frameworks. + * + * By using !important, the show and hide behavior will work as expected despite any clash between CSS selector + * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the + * styling to change how to hide an element then it is just a matter of using !important in their own CSS code. + * + * ### Overriding `.ng-hide` + * + * By default, the `.ng-hide` class will style the element with `display:none!important`. If you wish to change + * the hide behavior with ngShow/ngHide then this can be achieved by restating the styles for the `.ng-hide` + * class in CSS: + * + * ```css + * .ng-hide { + * //this is just another form of hiding an element + * display:block!important; + * position:absolute; + * top:-9999px; + * left:-9999px; + * } + * ``` + * + * By default you don't need to override in CSS anything and the animations will work around the display style. + * + * ## A note about animations with `ngShow` + * + * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression + * is true and false. This system works like the animation system present with ngClass except that + * you must also include the !important flag to override the display property + * so that you can perform an animation when the element is hidden during the time of the animation. + * + * ```css + * // + * //a working example can be found at the bottom of this page + * // + * .my-element.ng-hide-add, .my-element.ng-hide-remove { + * transition:0.5s linear all; + * } + * + * .my-element.ng-hide-add { ... } + * .my-element.ng-hide-add.ng-hide-add-active { ... } + * .my-element.ng-hide-remove { ... } + * .my-element.ng-hide-remove.ng-hide-remove-active { ... } + * ``` + * + * Keep in mind that, as of AngularJS version 1.2.17 (and 1.3.0-beta.11), there is no need to change the display + * property to block during animation states--ngAnimate will handle the style toggling automatically for you. + * + * @animations + * addClass: `.ng-hide` - happens after the `ngShow` expression evaluates to a truthy value and the just before contents are set to visible + * removeClass: `.ng-hide` - happens after the `ngShow` expression evaluates to a non truthy value and just before the contents are set to hidden * * @element ANY * @param {expression} ngShow If the {@link guide/expression expression} is truthy * then the element is shown or hidden respectively. * * @example - - - Click me:
- Show: I show up when your checkbox is checked.
- Hide: I hide when your checkbox is checked. -
- - it('should check ng-show / ng-hide', function() { - expect(element('.doc-example-live span:first:hidden').count()).toEqual(1); - expect(element('.doc-example-live span:last:visible').count()).toEqual(1); - - input('checked').check(); - - expect(element('.doc-example-live span:first:visible').count()).toEqual(1); - expect(element('.doc-example-live span:last:hidden').count()).toEqual(1); - }); - -
+ + + Click me:
+
+ Show: +
+ I show up when your checkbox is checked. +
+
+
+ Hide: +
+ I hide when your checkbox is checked. +
+
+
+ + @import url(//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-glyphicons.css); + + + .animate-show { + -webkit-transition:all linear 0.5s; + transition:all linear 0.5s; + line-height:20px; + opacity:1; + padding:10px; + border:1px solid black; + background:white; + } + + .animate-show.ng-hide { + line-height:0; + opacity:0; + padding:0 10px; + } + + .check-element { + padding:10px; + border:1px solid black; + background:white; + } + + + var thumbsUp = element(by.css('span.glyphicon-thumbs-up')); + var thumbsDown = element(by.css('span.glyphicon-thumbs-down')); + + it('should check ng-show / ng-hide', function() { + expect(thumbsUp.isDisplayed()).toBeFalsy(); + expect(thumbsDown.isDisplayed()).toBeTruthy(); + + element(by.model('checked')).click(); + + expect(thumbsUp.isDisplayed()).toBeTruthy(); + expect(thumbsDown.isDisplayed()).toBeFalsy(); + }); + +
*/ -//TODO(misko): refactor to remove element from the DOM -var ngShowDirective = ngDirective(function(scope, element, attr){ - scope.$watch(attr.ngShow, function ngShowWatchAction(value){ - element.css('display', toBoolean(value) ? '' : 'none'); - }); -}); +var ngShowDirective = ['$animate', function($animate) { + return function(scope, element, attr) { + scope.$watch(attr.ngShow, function ngShowWatchAction(value){ + $animate[toBoolean(value) ? 'removeClass' : 'addClass'](element, 'ng-hide'); + }); + }; +}]; /** * @ngdoc directive - * @name ng.directive:ngHide + * @name ngHide * * @description - * The `ngHide` and `ngShow` directives hide or show a portion of the DOM tree (HTML) - * conditionally. + * The `ngHide` directive shows or hides the given HTML element based on the expression + * provided to the `ngHide` attribute. The element is shown or hidden by removing or adding + * the `ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined + * in AngularJS and sets the display style to none (using an !important flag). + * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}). + * + * ```html + * + *
+ * + * + *
+ * ``` + * + * When the `.ngHide` expression evaluates to true then the `.ng-hide` CSS class is added to the class attribute + * on the element causing it to become hidden. When false, the `.ng-hide` CSS class is removed + * from the element causing the element not to appear hidden. + * + *
+ * **Note:** Here is a list of values that ngHide will consider as a falsy value (case insensitive):
+ * "f" / "0" / "false" / "no" / "n" / "[]" + *
+ * + * ## Why is !important used? + * + * You may be wondering why !important is used for the `.ng-hide` CSS class. This is because the `.ng-hide` selector + * can be easily overridden by heavier selectors. For example, something as simple + * as changing the display style on a HTML list item would make hidden elements appear visible. + * This also becomes a bigger issue when dealing with CSS frameworks. + * + * By using !important, the show and hide behavior will work as expected despite any clash between CSS selector + * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the + * styling to change how to hide an element then it is just a matter of using !important in their own CSS code. + * + * ### Overriding `.ng-hide` + * + * By default, the `.ng-hide` class will style the element with `display:none!important`. If you wish to change + * the hide behavior with ngShow/ngHide then this can be achieved by restating the styles for the `.ng-hide` + * class in CSS: + * + * ```css + * .ng-hide { + * //this is just another form of hiding an element + * display:block!important; + * position:absolute; + * top:-9999px; + * left:-9999px; + * } + * ``` + * + * By default you don't need to override in CSS anything and the animations will work around the display style. + * + * ## A note about animations with `ngHide` + * + * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression + * is true and false. This system works like the animation system present with ngClass, except that the `.ng-hide` + * CSS class is added and removed for you instead of your own CSS class. + * + * ```css + * // + * //a working example can be found at the bottom of this page + * // + * .my-element.ng-hide-add, .my-element.ng-hide-remove { + * transition:0.5s linear all; + * } + * + * .my-element.ng-hide-add { ... } + * .my-element.ng-hide-add.ng-hide-add-active { ... } + * .my-element.ng-hide-remove { ... } + * .my-element.ng-hide-remove.ng-hide-remove-active { ... } + * ``` + * + * Keep in mind that, as of AngularJS version 1.2.17 (and 1.3.0-beta.11), there is no need to change the display + * property to block during animation states--ngAnimate will handle the style toggling automatically for you. + * + * @animations + * removeClass: `.ng-hide` - happens after the `ngHide` expression evaluates to a truthy value and just before the contents are set to hidden + * addClass: `.ng-hide` - happens after the `ngHide` expression evaluates to a non truthy value and just before the contents are set to visible * * @element ANY * @param {expression} ngHide If the {@link guide/expression expression} is truthy then * the element is shown or hidden respectively. * * @example - - - Click me:
- Show: I show up when you checkbox is checked?
- Hide: I hide when you checkbox is checked? -
- - it('should check ng-show / ng-hide', function() { - expect(element('.doc-example-live span:first:hidden').count()).toEqual(1); - expect(element('.doc-example-live span:last:visible').count()).toEqual(1); - - input('checked').check(); - - expect(element('.doc-example-live span:first:visible').count()).toEqual(1); - expect(element('.doc-example-live span:last:hidden').count()).toEqual(1); - }); - -
+ + + Click me:
+
+ Show: +
+ I show up when your checkbox is checked. +
+
+
+ Hide: +
+ I hide when your checkbox is checked. +
+
+
+ + @import url(//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-glyphicons.css); + + + .animate-hide { + -webkit-transition:all linear 0.5s; + transition:all linear 0.5s; + line-height:20px; + opacity:1; + padding:10px; + border:1px solid black; + background:white; + } + + .animate-hide.ng-hide { + line-height:0; + opacity:0; + padding:0 10px; + } + + .check-element { + padding:10px; + border:1px solid black; + background:white; + } + + + var thumbsUp = element(by.css('span.glyphicon-thumbs-up')); + var thumbsDown = element(by.css('span.glyphicon-thumbs-down')); + + it('should check ng-show / ng-hide', function() { + expect(thumbsUp.isDisplayed()).toBeFalsy(); + expect(thumbsDown.isDisplayed()).toBeTruthy(); + + element(by.model('checked')).click(); + + expect(thumbsUp.isDisplayed()).toBeTruthy(); + expect(thumbsDown.isDisplayed()).toBeFalsy(); + }); + +
*/ -//TODO(misko): refactor to remove element from the DOM -var ngHideDirective = ngDirective(function(scope, element, attr){ - scope.$watch(attr.ngHide, function ngHideWatchAction(value){ - element.css('display', toBoolean(value) ? 'none' : ''); - }); -}); +var ngHideDirective = ['$animate', function($animate) { + return function(scope, element, attr) { + scope.$watch(attr.ngHide, function ngHideWatchAction(value){ + $animate[toBoolean(value) ? 'addClass' : 'removeClass'](element, 'ng-hide'); + }); + }; +}]; /** * @ngdoc directive - * @name ng.directive:ngStyle + * @name ngStyle + * @restrict AC * * @description * The `ngStyle` directive allows you to set CSS style on an HTML element conditionally. * * @element ANY - * @param {expression} ngStyle {@link guide/expression Expression} which evals to an - * object whose keys are CSS style names and values are corresponding values for those CSS - * keys. + * @param {expression} ngStyle + * + * {@link guide/expression Expression} which evals to an + * object whose keys are CSS style names and values are corresponding values for those CSS + * keys. + * + * Since some CSS style names are not valid keys for an object, they must be quoted. + * See the 'background-color' style in the example below. * * @example - + +
Sample Text @@ -13857,13 +21097,15 @@ var ngHideDirective = ngDirective(function(scope, element, attr){ color: black; }
- + + var colorSpan = element(by.css('span')); + it('should check ng-style', function() { - expect(element('.doc-example-live span').css('color')).toBe('rgb(0, 0, 0)'); - element('.doc-example-live :button[value=set]').click(); - expect(element('.doc-example-live span').css('color')).toBe('rgb(255, 0, 0)'); - element('.doc-example-live :button[value=clear]').click(); - expect(element('.doc-example-live span').css('color')).toBe('rgb(0, 0, 0)'); + expect(colorSpan.getCssValue('color')).toBe('rgba(0, 0, 0, 1)'); + element(by.css('input[value=\'set color\']')).click(); + expect(colorSpan.getCssValue('color')).toBe('rgba(255, 0, 0, 1)'); + element(by.css('input[value=clear]')).click(); + expect(colorSpan.getCssValue('color')).toBe('rgba(0, 0, 0, 1)'); });
@@ -13879,368 +21121,308 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { /** * @ngdoc directive - * @name ng.directive:ngSwitch + * @name ngSwitch * @restrict EA * * @description - * Conditionally change the DOM structure. + * The `ngSwitch` directive is used to conditionally swap DOM structure on your template based on a scope expression. + * Elements within `ngSwitch` but without `ngSwitchWhen` or `ngSwitchDefault` directives will be preserved at the location + * as specified in the template. + * + * The directive itself works similar to ngInclude, however, instead of downloading template code (or loading it + * from the template cache), `ngSwitch` simply chooses one of the nested elements and makes it visible based on which element + * matches the value obtained from the evaluated expression. In other words, you define a container element + * (where you place the directive), place an expression on the **`on="..."` attribute** + * (or the **`ng-switch="..."` attribute**), define any inner elements inside of the directive and place + * a when attribute per element. The when attribute is used to inform ngSwitch which element to display when the on + * expression is evaluated. If a matching expression is not found via a when attribute then an element with the default + * attribute is displayed. + * + *
+ * Be aware that the attribute values to match against cannot be expressions. They are interpreted + * as literal string values to match against. + * For example, **`ng-switch-when="someVal"`** will match against the string `"someVal"` not against the + * value of the expression `$scope.someVal`. + *
+ + * @animations + * enter - happens after the ngSwitch contents change and the matched child element is placed inside the container + * leave - happens just after the ngSwitch contents change and just before the former contents are removed from the DOM * * @usage + * + * ``` * * ... * ... - * ... * ... * + * ``` + * * * @scope + * @priority 800 * @param {*} ngSwitch|on expression to match against ng-switch-when. - * @paramDescription - * On child elments add: + * On child elements add: * * * `ngSwitchWhen`: the case statement to match against. If match then this - * case will be displayed. - * * `ngSwitchDefault`: the default case when no other casses match. + * case will be displayed. If the same match appears multiple times, all the + * elements will be displayed. + * * `ngSwitchDefault`: the default case when no other case match. If there + * are multiple default cases, all of them will be displayed when no other + * case match. + * * * @example - - - -
- - selection={{selection}} -
-
-
Settings Div
- Home Span - default -
+ + +
+ + selection={{selection}} +
+
+
Settings Div
+
Home Span
+
default
- - - it('should start in settings', function() { - expect(element('.doc-example-live [ng-switch]').text()).toMatch(/Settings Div/); - }); - it('should change to home', function() { - select('selection').option('home'); - expect(element('.doc-example-live [ng-switch]').text()).toMatch(/Home Span/); - }); - it('should select deafault', function() { - select('selection').option('other'); - expect(element('.doc-example-live [ng-switch]').text()).toMatch(/default/); - }); - - - */ -var NG_SWITCH = 'ng-switch'; -var ngSwitchDirective = valueFn({ - restrict: 'EA', - require: 'ngSwitch', - // asks for $scope to fool the BC controller module - controller: ['$scope', function ngSwitchController() { - this.cases = {}; - }], - link: function(scope, element, attr, ctrl) { - var watchExpr = attr.ngSwitch || attr.on, - selectedTransclude, - selectedElement, - selectedScope; - - scope.$watch(watchExpr, function ngSwitchWatchAction(value) { - if (selectedElement) { - selectedScope.$destroy(); - selectedElement.remove(); - selectedElement = selectedScope = null; - } - if ((selectedTransclude = ctrl.cases['!' + value] || ctrl.cases['?'])) { - scope.$eval(attr.change); - selectedScope = scope.$new(); - selectedTransclude(selectedScope, function(caseElement) { - selectedElement = caseElement; - element.append(caseElement); - }); +
+
+ + angular.module('switchExample', ['ngAnimate']) + .controller('ExampleController', ['$scope', function($scope) { + $scope.items = ['settings', 'home', 'other']; + $scope.selection = $scope.items[0]; + }]); + + + .animate-switch-container { + position:relative; + background:white; + border:1px solid black; + height:40px; + overflow:hidden; } - }); - } -}); + + .animate-switch { + padding:10px; + } + + .animate-switch.ng-animate { + -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + + position:absolute; + top:0; + left:0; + right:0; + bottom:0; + } + + .animate-switch.ng-leave.ng-leave-active, + .animate-switch.ng-enter { + top:-50px; + } + .animate-switch.ng-leave, + .animate-switch.ng-enter.ng-enter-active { + top:0; + } + + + var switchElem = element(by.css('[ng-switch]')); + var select = element(by.model('selection')); + + it('should start in settings', function() { + expect(switchElem.getText()).toMatch(/Settings Div/); + }); + it('should change to home', function() { + select.all(by.css('option')).get(1).click(); + expect(switchElem.getText()).toMatch(/Home Span/); + }); + it('should select default', function() { + select.all(by.css('option')).get(2).click(); + expect(switchElem.getText()).toMatch(/default/); + }); + +
+ */ +var ngSwitchDirective = ['$animate', function($animate) { + return { + restrict: 'EA', + require: 'ngSwitch', + + // asks for $scope to fool the BC controller module + controller: ['$scope', function ngSwitchController() { + this.cases = {}; + }], + link: function(scope, element, attr, ngSwitchController) { + var watchExpr = attr.ngSwitch || attr.on, + selectedTranscludes = [], + selectedElements = [], + previousElements = [], + selectedScopes = []; + + scope.$watch(watchExpr, function ngSwitchWatchAction(value) { + var i, ii; + for (i = 0, ii = previousElements.length; i < ii; ++i) { + previousElements[i].remove(); + } + previousElements.length = 0; + + for (i = 0, ii = selectedScopes.length; i < ii; ++i) { + var selected = selectedElements[i]; + selectedScopes[i].$destroy(); + previousElements[i] = selected; + $animate.leave(selected, function() { + previousElements.splice(i, 1); + }); + } + + selectedElements.length = 0; + selectedScopes.length = 0; + + if ((selectedTranscludes = ngSwitchController.cases['!' + value] || ngSwitchController.cases['?'])) { + scope.$eval(attr.change); + forEach(selectedTranscludes, function(selectedTransclude) { + var selectedScope = scope.$new(); + selectedScopes.push(selectedScope); + selectedTransclude.transclude(selectedScope, function(caseElement) { + var anchor = selectedTransclude.element; + + selectedElements.push(caseElement); + $animate.enter(caseElement, anchor.parent(), anchor); + }); + }); + } + }); + } + }; +}]; var ngSwitchWhenDirective = ngDirective({ transclude: 'element', - priority: 500, + priority: 800, require: '^ngSwitch', - compile: function(element, attrs, transclude) { - return function(scope, element, attr, ctrl) { - ctrl.cases['!' + attrs.ngSwitchWhen] = transclude; - }; + link: function(scope, element, attrs, ctrl, $transclude) { + ctrl.cases['!' + attrs.ngSwitchWhen] = (ctrl.cases['!' + attrs.ngSwitchWhen] || []); + ctrl.cases['!' + attrs.ngSwitchWhen].push({ transclude: $transclude, element: element }); } }); var ngSwitchDefaultDirective = ngDirective({ transclude: 'element', - priority: 500, + priority: 800, require: '^ngSwitch', - compile: function(element, attrs, transclude) { - return function(scope, element, attr, ctrl) { - ctrl.cases['?'] = transclude; - }; - } + link: function(scope, element, attr, ctrl, $transclude) { + ctrl.cases['?'] = (ctrl.cases['?'] || []); + ctrl.cases['?'].push({ transclude: $transclude, element: element }); + } }); /** * @ngdoc directive - * @name ng.directive:ngTransclude + * @name ngTransclude + * @restrict AC * * @description - * Insert the transcluded DOM here. + * Directive that marks the insertion point for the transcluded DOM of the nearest parent directive that uses transclusion. + * + * Any existing content of the element that this directive is placed on will be removed before the transcluded content is inserted. * * @element ANY * * @example - - + + -
+


{{text}}
- - + + it('should have transcluded', function() { - input('title').enter('TITLE'); - input('text').enter('TEXT'); - expect(binding('title')).toEqual('TITLE'); - expect(binding('text')).toEqual('TEXT'); + var titleElement = element(by.model('title')); + titleElement.clear(); + titleElement.sendKeys('TITLE'); + var textElement = element(by.model('text')); + textElement.clear(); + textElement.sendKeys('TEXT'); + expect(element(by.binding('title')).getText()).toEqual('TITLE'); + expect(element(by.binding('text')).getText()).toEqual('TEXT'); }); - - + + * */ var ngTranscludeDirective = ngDirective({ - controller: ['$transclude', '$element', function($transclude, $element) { + link: function($scope, $element, $attrs, controller, $transclude) { + if (!$transclude) { + throw minErr('ngTransclude')('orphan', + 'Illegal use of ngTransclude directive in the template! ' + + 'No parent directive that requires a transclusion found. ' + + 'Element: {0}', + startingTag($element)); + } + $transclude(function(clone) { + $element.empty(); $element.append(clone); }); - }] + } }); /** * @ngdoc directive - * @name ng.directive:ngView - * @restrict ECA - * - * @description - * # Overview - * `ngView` is a directive that complements the {@link ng.$route $route} service by - * including the rendered template of the current route into the main layout (`index.html`) file. - * Every time the current route changes, the included view changes with it according to the - * configuration of the `$route` service. - * - * @scope - * @example - - -
- Choose: - Moby | - Moby: Ch1 | - Gatsby | - Gatsby: Ch4 | - Scarlet Letter
- -
-
- -
$location.path() = {{$location.path()}}
-
$route.current.templateUrl = {{$route.current.templateUrl}}
-
$route.current.params = {{$route.current.params}}
-
$route.current.scope.name = {{$route.current.scope.name}}
-
$routeParams = {{$routeParams}}
-
-
- - - controller: {{name}}
- Book Id: {{params.bookId}}
-
- - - controller: {{name}}
- Book Id: {{params.bookId}}
- Chapter Id: {{params.chapterId}} -
- - - angular.module('ngView', [], function($routeProvider, $locationProvider) { - $routeProvider.when('/Book/:bookId', { - templateUrl: 'book.html', - controller: BookCntl - }); - $routeProvider.when('/Book/:bookId/ch/:chapterId', { - templateUrl: 'chapter.html', - controller: ChapterCntl - }); - - // configure html5 to get links working on jsfiddle - $locationProvider.html5Mode(true); - }); - - function MainCntl($scope, $route, $routeParams, $location) { - $scope.$route = $route; - $scope.$location = $location; - $scope.$routeParams = $routeParams; - } - - function BookCntl($scope, $routeParams) { - $scope.name = "BookCntl"; - $scope.params = $routeParams; - } - - function ChapterCntl($scope, $routeParams) { - $scope.name = "ChapterCntl"; - $scope.params = $routeParams; - } - - - - it('should load and compile correct template', function() { - element('a:contains("Moby: Ch1")').click(); - var content = element('.doc-example-live [ng-view]').text(); - expect(content).toMatch(/controller\: ChapterCntl/); - expect(content).toMatch(/Book Id\: Moby/); - expect(content).toMatch(/Chapter Id\: 1/); - - element('a:contains("Scarlet")').click(); - content = element('.doc-example-live [ng-view]').text(); - expect(content).toMatch(/controller\: BookCntl/); - expect(content).toMatch(/Book Id\: Scarlet/); - }); - -
- */ - - -/** - * @ngdoc event - * @name ng.directive:ngView#$viewContentLoaded - * @eventOf ng.directive:ngView - * @eventType emit on the current ngView scope - * @description - * Emitted every time the ngView content is reloaded. - */ -var ngViewDirective = ['$http', '$templateCache', '$route', '$anchorScroll', '$compile', - '$controller', - function($http, $templateCache, $route, $anchorScroll, $compile, - $controller) { - return { - restrict: 'ECA', - terminal: true, - link: function(scope, element, attr) { - var lastScope, - onloadExp = attr.onload || ''; - - scope.$on('$routeChangeSuccess', update); - update(); - - - function destroyLastScope() { - if (lastScope) { - lastScope.$destroy(); - lastScope = null; - } - } - - function clearContent() { - element.html(''); - destroyLastScope(); - } - - function update() { - var locals = $route.current && $route.current.locals, - template = locals && locals.$template; - - if (template) { - element.html(template); - destroyLastScope(); - - var link = $compile(element.contents()), - current = $route.current, - controller; - - lastScope = current.scope = scope.$new(); - if (current.controller) { - locals.$scope = lastScope; - controller = $controller(current.controller, locals); - element.children().data('$ngControllerController', controller); - } - - link(lastScope); - lastScope.$emit('$viewContentLoaded'); - lastScope.$eval(onloadExp); - - // $anchorScroll might listen on event... - $anchorScroll(); - } else { - clearContent(); - } - } - } - }; -}]; - -/** - * @ngdoc directive - * @name ng.directive:script + * @name script + * @restrict E * * @description - * Load content of a script tag, with type `text/ng-template`, into `$templateCache`, so that the - * template can be used by `ngInclude`, `ngView` or directive templates. + * Load the content of a ` Load inlined template
- - + + it('should load template defined inside script tag', function() { - element('#tpl-link').click(); - expect(element('#tpl-content').text()).toMatch(/Content of the template/); + element(by.css('#tpl-link')).click(); + expect(element(by.css('#tpl-content')).getText()).toMatch(/Content of the template/); }); - - + + */ var scriptDirective = ['$templateCache', function($templateCache) { return { @@ -14258,9 +21440,10 @@ var scriptDirective = ['$templateCache', function($templateCache) { }; }]; +var ngOptionsMinErr = minErr('ngOptions'); /** * @ngdoc directive - * @name ng.directive:select + * @name select * @restrict E * * @description @@ -14268,22 +21451,29 @@ var scriptDirective = ['$templateCache', function($templateCache) { * * # `ngOptions` * - * Optionally `ngOptions` attribute can be used to dynamically generate a list of `
-
- + + it('should check ng-options', function() { - expect(binding('{selected_color:color}')).toMatch('red'); - select('color').option('0'); - expect(binding('{selected_color:color}')).toMatch('black'); - using('.nullable').select('color').option(''); - expect(binding('{selected_color:color}')).toMatch('null'); + expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('red'); + element.all(by.model('myColor')).first().click(); + element.all(by.css('select[ng-model="myColor"] option')).first().click(); + expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('black'); + element(by.css('.nullable select[ng-model="myColor"]')).click(); + element.all(by.css('.nullable select[ng-model="myColor"] option')).first().click(); + expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('null'); }); - -
+ + */ var ngOptionsDirective = valueFn({ terminal: true }); +// jshint maxlen: false var selectDirective = ['$compile', '$parse', function($compile, $parse) { - //0000111110000000000022220000000000000000000000333300000000000000444444444444444440000000005555555555555555500000006666666666666666600000000000000077770 - var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w\d]*)|(?:\(\s*([\$\w][\$\w\d]*)\s*,\s*([\$\w][\$\w\d]*)\s*\)))\s+in\s+(.*)$/, + //000011111111110000000000022222222220000000000000000000003333333333000000000000004444444444444440000000005555555555555550000000666666666666666000000000000000777777777700000000000000000008888888888 + var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/, nullModelCtrl = {$setViewValue: noop}; +// jshint maxlen: 100 return { restrict: 'E', @@ -14403,10 +21601,11 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { ngModelCtrl = ngModelCtrl_; nullOption = nullOption_; unknownOption = unknownOption_; - } + }; self.addOption = function(value) { + assertNotHasOwnProperty(value, '"option value"'); optionsMap[value] = true; if (ngModelCtrl.$viewValue == value) { @@ -14432,12 +21631,12 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { $element.prepend(unknownOption); $element.val(unknownVal); unknownOption.prop('selected', true); // needed for IE - } + }; self.hasOption = function(value) { return optionsMap.hasOwnProperty(value); - } + }; $scope.$on('$destroy', function() { // disable unknown option so that we don't do work when the whole select is being destroyed @@ -14463,7 +21662,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { // find "null" option for(var i = 0, children = element.children(), ii = children.length; i < ii; i++) { - if (children[i].value == '') { + if (children[i].value === '') { emptyOption = nullOption = children.eq(i); break; } @@ -14472,30 +21671,22 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { selectCtrl.init(ngModelCtrl, nullOption, unknownOption); // required validator - if (multiple && (attr.required || attr.ngRequired)) { - var requiredValidator = function(value) { - ngModelCtrl.$setValidity('required', !attr.required || (value && value.length)); - return value; + if (multiple) { + ngModelCtrl.$isEmpty = function(value) { + return !value || value.length === 0; }; - - ngModelCtrl.$parsers.push(requiredValidator); - ngModelCtrl.$formatters.unshift(requiredValidator); - - attr.$observe('required', function() { - requiredValidator(ngModelCtrl.$viewValue); - }); } - if (optionsExp) Options(scope, element, ngModelCtrl); - else if (multiple) Multiple(scope, element, ngModelCtrl); - else Single(scope, element, ngModelCtrl, selectCtrl); + if (optionsExp) setupAsOptions(scope, element, ngModelCtrl); + else if (multiple) setupAsMultiple(scope, element, ngModelCtrl); + else setupAsSingle(scope, element, ngModelCtrl, selectCtrl); //////////////////////////// - function Single(scope, selectElement, ngModelCtrl, selectCtrl) { + function setupAsSingle(scope, selectElement, ngModelCtrl, selectCtrl) { ngModelCtrl.$render = function() { var viewValue = ngModelCtrl.$viewValue; @@ -14512,7 +21703,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { } }; - selectElement.bind('change', function() { + selectElement.on('change', function() { scope.$apply(function() { if (unknownOption.parent()) unknownOption.remove(); ngModelCtrl.$setViewValue(selectElement.val()); @@ -14520,7 +21711,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { }); } - function Multiple(scope, selectElement, ctrl) { + function setupAsMultiple(scope, selectElement, ctrl) { var lastView; ctrl.$render = function() { var items = new HashMap(ctrl.$viewValue); @@ -14533,12 +21724,12 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { // we need to work of an array, so we need to see if anything was inserted/removed scope.$watch(function selectMultipleWatch() { if (!equals(lastView, ctrl.$viewValue)) { - lastView = copy(ctrl.$viewValue); + lastView = shallowCopy(ctrl.$viewValue); ctrl.$render(); } }); - selectElement.bind('change', function() { + selectElement.on('change', function() { scope.$apply(function() { var array = []; forEach(selectElement.find('option'), function(option) { @@ -14551,13 +21742,15 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { }); } - function Options(scope, selectElement, ctrl) { + function setupAsOptions(scope, selectElement, ctrl) { var match; - if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) { - throw Error( - "Expected ngOptions in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" + - " but got '" + optionsExp + "'."); + if (!(match = optionsExp.match(NG_OPTIONS_REGEXP))) { + throw ngOptionsMinErr('iexp', + "Expected expression in form of " + + "'_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" + + " but got '{0}'. Element: {1}", + optionsExp, startingTag(selectElement)); } var displayFn = $parse(match[2] || match[1]), @@ -14566,9 +21759,12 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { groupByFn = $parse(match[3] || ''), valueFn = $parse(match[2] ? match[1] : valueName), valuesFn = $parse(match[7]), - // This is an array of array of existing option groups in DOM. We try to reuse these if possible - // optionGroupsCache[0] is the options with no option group - // optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element + track = match[8], + trackFn = track ? $parse(match[8]) : null, + // This is an array of array of existing option groups in DOM. + // We try to reuse these if possible + // - optionGroupsCache[0] is the options with no option group + // - optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element optionGroupsCache = [[{element: selectElement, label:''}]]; if (nullOption) { @@ -14579,20 +21775,20 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { // becomes the compilation root nullOption.removeClass('ng-scope'); - // we need to remove it before calling selectElement.html('') because otherwise IE will + // we need to remove it before calling selectElement.empty() because otherwise IE will // remove the label from the element. wtf? nullOption.remove(); } // clear contents, we'll add what's needed based on the model - selectElement.html(''); + selectElement.empty(); - selectElement.bind('change', function() { + selectElement.on('change', function() { scope.$apply(function() { var optionGroup, collection = valuesFn(scope) || [], locals = {}, - key, value, optionElement, index, groupIndex, length, groupLength; + key, value, optionElement, index, groupIndex, length, groupLength, trackIndex; if (multiple) { value = []; @@ -14606,7 +21802,14 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { if ((optionElement = optionGroup[index].element)[0].selected) { key = optionElement.val(); if (keyName) locals[keyName] = key; - locals[valueName] = collection[key]; + if (trackFn) { + for (trackIndex = 0; trackIndex < collection.length; trackIndex++) { + locals[valueName] = collection[trackIndex]; + if (trackFn(scope, locals) == key) break; + } + } else { + locals[valueName] = collection[key]; + } value.push(valueFn(scope, locals)); } } @@ -14615,25 +21818,71 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { key = selectElement.val(); if (key == '?') { value = undefined; - } else if (key == ''){ + } else if (key === ''){ value = null; } else { - locals[valueName] = collection[key]; - if (keyName) locals[keyName] = key; - value = valueFn(scope, locals); + if (trackFn) { + for (trackIndex = 0; trackIndex < collection.length; trackIndex++) { + locals[valueName] = collection[trackIndex]; + if (trackFn(scope, locals) == key) { + value = valueFn(scope, locals); + break; + } + } + } else { + locals[valueName] = collection[key]; + if (keyName) locals[keyName] = key; + value = valueFn(scope, locals); + } } } ctrl.$setViewValue(value); + render(); }); }); ctrl.$render = render; - // TODO(vojta): can't we optimize this ? - scope.$watch(render); + scope.$watchCollection(valuesFn, render); + scope.$watchCollection(function () { + var locals = {}, + values = valuesFn(scope); + if (values) { + var toDisplay = new Array(values.length); + for (var i = 0, ii = values.length; i < ii; i++) { + locals[valueName] = values[i]; + toDisplay[i] = displayFn(scope, locals); + } + return toDisplay; + } + }, render); + + if ( multiple ) { + scope.$watchCollection(function() { return ctrl.$modelValue; }, render); + } + + function getSelectedSet() { + var selectedSet = false; + if (multiple) { + var modelValue = ctrl.$modelValue; + if (trackFn && isArray(modelValue)) { + selectedSet = new HashMap([]); + var locals = {}; + for (var trackIndex = 0; trackIndex < modelValue.length; trackIndex++) { + locals[valueName] = modelValue[trackIndex]; + selectedSet.put(trackFn(scope, locals), modelValue[trackIndex]); + } + } else { + selectedSet = new HashMap(modelValue); + } + } + return selectedSet; + } + function render() { - var optionGroups = {'':[]}, // Temporary location for the option groups before we render them + // Temporary location for the option groups before we render them + var optionGroups = {'':[]}, optionGroupNames = [''], optionGroupName, optionGroup, @@ -14642,37 +21891,55 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { modelValue = ctrl.$modelValue, values = valuesFn(scope) || [], keys = keyName ? sortedKeys(values) : values, + key, groupLength, length, groupIndex, index, locals = {}, selected, - selectedSet = false, // nothing is selected yet + selectedSet = getSelectedSet(), lastElement, element, label; - if (multiple) { - selectedSet = new HashMap(modelValue); - } // We now build up the list of options we need (we merge later) for (index = 0; length = keys.length, index < length; index++) { - locals[valueName] = values[keyName ? locals[keyName]=keys[index]:index]; - optionGroupName = groupByFn(scope, locals) || ''; + + key = index; + if (keyName) { + key = keys[index]; + if ( key.charAt(0) === '$' ) continue; + locals[keyName] = key; + } + + locals[valueName] = values[key]; + + optionGroupName = groupByFn(scope, locals) || ''; if (!(optionGroup = optionGroups[optionGroupName])) { optionGroup = optionGroups[optionGroupName] = []; optionGroupNames.push(optionGroupName); } if (multiple) { - selected = selectedSet.remove(valueFn(scope, locals)) != undefined; + selected = isDefined( + selectedSet.remove(trackFn ? trackFn(scope, locals) : valueFn(scope, locals)) + ); } else { - selected = modelValue === valueFn(scope, locals); + if (trackFn) { + var modelCast = {}; + modelCast[valueName] = modelValue; + selected = trackFn(scope, modelCast) === trackFn(scope, locals); + } else { + selected = modelValue === valueFn(scope, locals); + } selectedSet = selectedSet || selected; // see if at least one item is selected } label = displayFn(scope, locals); // what will be seen by the user - label = label === undefined ? '' : label; // doing displayFn(scope, locals) || '' overwrites zero values + + // doing displayFn(scope, locals) || '' overwrites zero values + label = isDefined(label) ? label : ''; optionGroup.push({ - id: keyName ? keys[index] : index, // either the index into array or key from object + // either the index into array or key from object + id: trackFn ? trackFn(scope, locals) : (keyName ? keys[index] : index), label: label, selected: selected // determine if we should be selected }); @@ -14724,6 +21991,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { lastElement = existingOption.element; if (existingOption.label !== option.label) { lastElement.text(existingOption.label = option.label); + lastElement.prop('label', existingOption.label); } if (existingOption.id !== option.id) { lastElement.val(existingOption.id = option.id); @@ -14731,6 +21999,12 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { // lastElement.prop('selected') provided by jQuery has side-effects if (lastElement[0].selected !== option.selected) { lastElement.prop('selected', (existingOption.selected = option.selected)); + if (msie) { + // See #7692 + // The selected item wouldn't visually update on IE without this. + // Tested on Win7: IE9, IE10 and IE11. Future IEs should be tested as well + lastElement.prop('selected', existingOption.selected); + } } } else { // grow elements @@ -14745,7 +22019,9 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { // rather then the element. (element = optionTemplate.clone()) .val(option.id) + .prop('selected', option.selected) .attr('selected', option.selected) + .prop('label', option.label) .text(option.label); } @@ -14755,6 +22031,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { id: option.id, selected: option.selected }); + selectCtrl.addOption(option.label, element); if (lastElement) { lastElement.after(element); } else { @@ -14766,7 +22043,9 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { // remove any excessive OPTIONs in a group index++; // increment since the existingOptions[0] is parent element not OPTION while(existingOptions.length > index) { - existingOptions.pop().element.remove(); + option = existingOptions.pop(); + selectCtrl.removeOption(option.label); + option.element.remove(); } } // remove any excessive OPTGROUPs from select @@ -14776,7 +22055,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { } } } - } + }; }]; var optionDirective = ['$interpolate', function($interpolate) { @@ -14820,12 +22099,12 @@ var optionDirective = ['$interpolate', function($interpolate) { selectCtrl.addOption(attr.value); } - element.bind('$destroy', function() { + element.on('$destroy', function() { selectCtrl.removeOption(attr.value); }); }; } - } + }; }]; var styleDirective = valueFn({ @@ -14833,6 +22112,12 @@ var styleDirective = valueFn({ terminal: true }); + if (window.angular.bootstrap) { + //AngularJS is already loaded, so we can return here... + console.log('WARNING: Tried to load angular more than once.'); + return; + } + //try to bind to jquery now so that one can write angular.element().read() //but we will rebind on bootstrap again. bindJQuery(); @@ -14844,4 +22129,5 @@ var styleDirective = valueFn({ }); })(window, document); -angular.element(document).find('head').append(''); \ No newline at end of file + +!window.angular.$$csp() && window.angular.element(document).find('head').prepend(''); \ No newline at end of file diff --git a/bower_components/angular/angular.min.js b/bower_components/angular/angular.min.js index 2b220688..ae1cd712 100644 --- a/bower_components/angular/angular.min.js +++ b/bower_components/angular/angular.min.js @@ -1,163 +1,217 @@ /* - AngularJS v1.0.7 - (c) 2010-2012 Google, Inc. http://angularjs.org + AngularJS v1.2.27 + (c) 2010-2014 Google, Inc. http://angularjs.org License: MIT */ -(function(P,T,q){'use strict';function m(b,a,c){var d;if(b)if(H(b))for(d in b)d!="prototype"&&d!="length"&&d!="name"&&b.hasOwnProperty(d)&&a.call(c,b[d],d);else if(b.forEach&&b.forEach!==m)b.forEach(a,c);else if(!b||typeof b.length!=="number"?0:typeof b.hasOwnProperty!="function"&&typeof b.constructor!="function"||b instanceof K||ca&&b instanceof ca||wa.call(b)!=="[object Object]"||typeof b.callee==="function")for(d=0;d=0&&b.splice(c,1);return a}function U(b,a){if(oa(b)||b&&b.$evalAsync&&b.$watch)throw Error("Can't copy Window or Scope");if(a){if(b===a)throw Error("Can't copy equivalent objects or arrays");if(E(b))for(var c=a.length=0;c2?ha.call(arguments,2):[];return H(a)&&!(a instanceof RegExp)?c.length?function(){return arguments.length?a.apply(b,c.concat(ha.call(arguments,0))):a.apply(b,c)}:function(){return arguments.length?a.apply(b,arguments):a.call(b)}:a}function ic(b,a){var c=a;/^\$+/.test(b)?c=q:oa(a)?c="$WINDOW":a&&T===a?c="$DOCUMENT":a&&a.$evalAsync&&a.$watch&&(c="$SCOPE");return c}function da(b,a){return JSON.stringify(b, -ic,a?" ":null)}function pb(b){return B(b)?JSON.parse(b):b}function Ua(b){b&&b.length!==0?(b=z(""+b),b=!(b=="f"||b=="0"||b=="false"||b=="no"||b=="n"||b=="[]")):b=!1;return b}function pa(b){b=u(b).clone();try{b.html("")}catch(a){}var c=u("
").append(b).html();try{return b[0].nodeType===3?z(c):c.match(/^(<[^>]+>)/)[1].replace(/^<([\w\-]+)/,function(a,b){return"<"+z(b)})}catch(d){return z(c)}}function Va(b){var a={},c,d;m((b||"").split("&"),function(b){b&&(c=b.split("="),d=decodeURIComponent(c[0]), -a[d]=y(c[1])?decodeURIComponent(c[1]):!0)});return a}function qb(b){var a=[];m(b,function(b,d){a.push(Wa(d,!0)+(b===!0?"":"="+Wa(b,!0)))});return a.length?a.join("&"):""}function Xa(b){return Wa(b,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function Wa(b,a){return encodeURIComponent(b).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,a?"%20":"+")}function jc(b,a){function c(a){a&&d.push(a)}var d=[b],e,g,h=["ng:app","ng-app","x-ng-app", -"data-ng-app"],f=/\sng[:\-]app(:\s*([\w\d_]+);?)?\s/;m(h,function(a){h[a]=!0;c(T.getElementById(a));a=a.replace(":","\\:");b.querySelectorAll&&(m(b.querySelectorAll("."+a),c),m(b.querySelectorAll("."+a+"\\:"),c),m(b.querySelectorAll("["+a+"]"),c))});m(d,function(a){if(!e){var b=f.exec(" "+a.className+" ");b?(e=a,g=(b[2]||"").replace(/\s+/g,",")):m(a.attributes,function(b){if(!e&&h[b.name])e=a,g=b.value})}});e&&a(e,g?[g]:[])}function rb(b,a){var c=function(){b=u(b);a=a||[];a.unshift(["$provide",function(a){a.value("$rootElement", -b)}]);a.unshift("ng");var c=sb(a);c.invoke(["$rootScope","$rootElement","$compile","$injector",function(a,b,c,d){a.$apply(function(){b.data("$injector",d);c(b)(a)})}]);return c},d=/^NG_DEFER_BOOTSTRAP!/;if(P&&!d.test(P.name))return c();P.name=P.name.replace(d,"");Ya.resumeBootstrap=function(b){m(b,function(b){a.push(b)});c()}}function Za(b,a){a=a||"_";return b.replace(kc,function(b,d){return(d?a:"")+b.toLowerCase()})}function $a(b,a,c){if(!b)throw Error("Argument '"+(a||"?")+"' is "+(c||"required")); -return b}function qa(b,a,c){c&&E(b)&&(b=b[b.length-1]);$a(H(b),a,"not a function, got "+(b&&typeof b=="object"?b.constructor.name||"Object":typeof b));return b}function lc(b){function a(a,b,e){return a[b]||(a[b]=e())}return a(a(b,"angular",Object),"module",function(){var b={};return function(d,e,g){e&&b.hasOwnProperty(d)&&(b[d]=null);return a(b,d,function(){function a(c,d,e){return function(){b[e||"push"]([c,d,arguments]);return k}}if(!e)throw Error("No module: "+d);var b=[],c=[],j=a("$injector", -"invoke"),k={_invokeQueue:b,_runBlocks:c,requires:e,name:d,provider:a("$provide","provider"),factory:a("$provide","factory"),service:a("$provide","service"),value:a("$provide","value"),constant:a("$provide","constant","unshift"),filter:a("$filterProvider","register"),controller:a("$controllerProvider","register"),directive:a("$compileProvider","directive"),config:j,run:function(a){c.push(a);return this}};g&&j(g);return k})}})}function tb(b){return b.replace(mc,function(a,b,d,e){return e?d.toUpperCase(): -d}).replace(nc,"Moz$1")}function ab(b,a){function c(){var e;for(var b=[this],c=a,h,f,i,j,k,l;b.length;){h=b.shift();f=0;for(i=h.length;f 
"+b;a.removeChild(a.firstChild);bb(this,a.childNodes);this.remove()}else bb(this,b)}function cb(b){return b.cloneNode(!0)}function ra(b){ub(b);for(var a=0,b=b.childNodes||[];a-1}function xb(b,a){a&&m(a.split(" "),function(a){b.className=Q((" "+b.className+" ").replace(/[\n\t]/g," ").replace(" "+Q(a)+" "," "))})} -function yb(b,a){a&&m(a.split(" "),function(a){if(!Ca(b,a))b.className=Q(b.className+" "+Q(a))})}function bb(b,a){if(a)for(var a=!a.nodeName&&y(a.length)&&!oa(a)?a:[a],c=0;c4096&&c.warn("Cookie '"+a+"' possibly not set or overflowed because it was too large ("+d+" > 4096 bytes)!")}else{if(i.cookie!==$){$=i.cookie;d=$.split("; ");r={};for(f=0;f0&&(a=unescape(e.substring(0,j)),r[a]===q&&(r[a]=unescape(e.substring(j+1))))}return r}};f.defer=function(a,b){var c; -p++;c=l(function(){delete o[c];e(a)},b||0);o[c]=!0;return c};f.defer.cancel=function(a){return o[a]?(delete o[a],n(a),e(C),!0):!1}}function wc(){this.$get=["$window","$log","$sniffer","$document",function(b,a,c,d){return new vc(b,d,a,c)}]}function xc(){this.$get=function(){function b(b,d){function e(a){if(a!=l){if(n){if(n==a)n=a.n}else n=a;g(a.n,a.p);g(a,l);l=a;l.n=null}}function g(a,b){if(a!=b){if(a)a.p=b;if(b)b.n=a}}if(b in a)throw Error("cacheId "+b+" taken");var h=0,f=v({},d,{id:b}),i={},j=d&& -d.capacity||Number.MAX_VALUE,k={},l=null,n=null;return a[b]={put:function(a,b){var c=k[a]||(k[a]={key:a});e(c);w(b)||(a in i||h++,i[a]=b,h>j&&this.remove(n.key))},get:function(a){var b=k[a];if(b)return e(b),i[a]},remove:function(a){var b=k[a];if(b){if(b==l)l=b.p;if(b==n)n=b.n;g(b.n,b.p);delete k[a];delete i[a];h--}},removeAll:function(){i={};h=0;k={};l=n=null},destroy:function(){k=f=i=null;delete a[b]},info:function(){return v({},f,{size:h})}}}var a={};b.info=function(){var b={};m(a,function(a,e){b[e]= -a.info()});return b};b.get=function(b){return a[b]};return b}}function yc(){this.$get=["$cacheFactory",function(b){return b("templates")}]}function Db(b){var a={},c="Directive",d=/^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/,e=/(([\d\w\-_]+)(?:\:([^;]+))?;?)/,g="Template must have exactly one root element. was: ",h=/^\s*(https?|ftp|mailto|file):/;this.directive=function i(d,e){B(d)?($a(e,"directive"),a.hasOwnProperty(d)||(a[d]=[],b.factory(d+c,["$injector","$exceptionHandler",function(b,c){var e=[];m(a[d], -function(a){try{var g=b.invoke(a);if(H(g))g={compile:I(g)};else if(!g.compile&&g.link)g.compile=I(g.link);g.priority=g.priority||0;g.name=g.name||d;g.require=g.require||g.controller&&g.name;g.restrict=g.restrict||"A";e.push(g)}catch(h){c(h)}});return e}])),a[d].push(e)):m(d,nb(i));return this};this.urlSanitizationWhitelist=function(a){return y(a)?(h=a,this):h};this.$get=["$injector","$interpolate","$exceptionHandler","$http","$templateCache","$parse","$controller","$rootScope","$document",function(b, -j,k,l,n,o,p,s,t){function x(a,b,c){a instanceof u||(a=u(a));m(a,function(b,c){b.nodeType==3&&b.nodeValue.match(/\S+/)&&(a[c]=u(b).wrap("").parent()[0])});var d=A(a,b,a,c);return function(b,c){$a(b,"scope");for(var e=c?ua.clone.call(a):a,j=0,g=e.length;jr.priority)break;if(Y=r.scope)ta("isolated scope",J,r,D),L(Y)&&(M(D,"ng-isolate-scope"),J=r),M(D,"ng-scope"),s=s||r;F=r.name;if(Y=r.controller)y=y||{},ta("'"+F+"' controller",y[F],r,D),y[F]=r;if(Y=r.transclude)ta("transclusion",ja,r,D),ja=r,l=r.priority,Y=="element"?(W=u(b),D=c.$$element=u(T.createComment(" "+ -F+": "+c[F]+" ")),b=D[0],C(e,u(W[0]),b),V=x(W,d,l)):(W=u(cb(b)).contents(),D.html(""),V=x(W,d));if(Y=r.template)if(ta("template",A,r,D),A=r,Y=Fb(Y),r.replace){W=u("
"+Q(Y)+"
").contents();b=W[0];if(W.length!=1||b.nodeType!==1)throw Error(g+Y);C(e,D,b);F={$attr:{}};a=a.concat(N(b,a.splice(v+1,a.length-(v+1)),F));$(c,F);z=a.length}else D.html(Y);if(r.templateUrl)ta("template",A,r,D),A=r,i=R(a.splice(v,a.length-v),i,D,c,e,r.replace,V),z=a.length;else if(r.compile)try{w=r.compile(D,c,V),H(w)? -j(null,w):w&&j(w.pre,w.post)}catch(G){k(G,pa(D))}if(r.terminal)i.terminal=!0,l=Math.max(l,r.priority)}i.scope=s&&s.scope;i.transclude=ja&&V;return i}function r(d,e,g,j){var h=!1;if(a.hasOwnProperty(e))for(var o,e=b.get(e+c),l=0,p=e.length;lo.priority)&&o.restrict.indexOf(g)!=-1)d.push(o),h=!0}catch(n){k(n)}return h}function $(a,b){var c=b.$attr,d=a.$attr,e=a.$$element;m(a,function(d,e){e.charAt(0)!="$"&&(b[e]&&(d+=(e==="style"?";":" ")+b[e]),a.$set(e,d,!0,c[e]))});m(b, -function(b,g){g=="class"?(M(e,b),a["class"]=(a["class"]?a["class"]+" ":"")+b):g=="style"?e.attr("style",e.attr("style")+";"+b):g.charAt(0)!="$"&&!a.hasOwnProperty(g)&&(a[g]=b,d[g]=c[g])})}function R(a,b,c,d,e,j,h){var i=[],k,o,p=c[0],t=a.shift(),s=v({},t,{controller:null,templateUrl:null,transclude:null,scope:null});c.html("");l.get(t.templateUrl,{cache:n}).success(function(l){var n,t,l=Fb(l);if(j){t=u("
"+Q(l)+"
").contents();n=t[0];if(t.length!=1||n.nodeType!==1)throw Error(g+l);l={$attr:{}}; -C(e,c,n);N(n,a,l);$(d,l)}else n=p,c.html(l);a.unshift(s);k=J(a,n,d,h);for(o=A(c[0].childNodes,h);i.length;){var r=i.pop(),l=i.pop();t=i.pop();var ia=i.pop(),D=n;t!==p&&(D=cb(n),C(l,u(t),D));k(function(){b(o,ia,D,e,r)},ia,D,e,r)}i=null}).error(function(a,b,c,d){throw Error("Failed to load template: "+d.url);});return function(a,c,d,e,g){i?(i.push(c),i.push(d),i.push(e),i.push(g)):k(function(){b(o,c,d,e,g)},c,d,e,g)}}function F(a,b){return b.priority-a.priority}function ta(a,b,c,d){if(b)throw Error("Multiple directives ["+ -b.name+", "+c.name+"] asking for "+a+" on: "+pa(d));}function y(a,b){var c=j(b,!0);c&&a.push({priority:0,compile:I(function(a,b){var d=b.parent(),e=d.data("$binding")||[];e.push(c);M(d.data("$binding",e),"ng-binding");a.$watch(c,function(a){b[0].nodeValue=a})})})}function V(a,b,c,d){var e=j(c,!0);e&&b.push({priority:100,compile:I(function(a,b,c){b=c.$$observers||(c.$$observers={});d==="class"&&(e=j(c[d],!0));c[d]=q;(b[d]||(b[d]=[])).$$inter=!0;(c.$$observers&&c.$$observers[d].$$scope||a).$watch(e, -function(a){c.$set(d,a)})})})}function C(a,b,c){var d=b[0],e=d.parentNode,g,j;if(a){g=0;for(j=a.length;g -0){var e=R[0],f=e.text;if(f==a||f==b||f==c||f==d||!a&&!b&&!c&&!d)return e}return!1}function f(b,c,d,f){return(b=h(b,c,d,f))?(a&&!b.json&&e("is not valid json",b),R.shift(),b):!1}function i(a){f(a)||e("is unexpected, expecting ["+a+"]",h())}function j(a,b){return function(c,d){return a(c,d,b)}}function k(a,b,c){return function(d,e){return b(d,e,a,c)}}function l(){for(var a=[];;)if(R.length>0&&!h("}",")",";","]")&&a.push(w()),!f(";"))return a.length==1?a[0]:function(b,c){for(var d,e=0;e","<=",">="))a=k(a,b.fn,t());return a}function x(){for(var a=m(),b;b=f("*","/","%");)a=k(a,b.fn,m());return a}function m(){var a;return f("+")?A():(a=f("-"))?k(r,a.fn,m()):(a=f("!"))?j(a.fn,m()):A()}function A(){var a;if(f("("))a=w(),i(")");else if(f("["))a=N();else if(f("{"))a=J();else{var b=f();(a=b.fn)||e("not a primary expression",b)}for(var c;b=f("(","[",".");)b.text==="("?(a=y(a,c),c=null):b.text==="["?(c=a,a=V(a)):b.text==="."?(c=a,a=u(a)):e("IMPOSSIBLE");return a}function N(){var a= -[];if(g().text!="]"){do a.push(F());while(f(","))}i("]");return function(b,c){for(var d=[],e=0;e1;d++){var e=a.shift(),g=b[e];g||(g={},b[e]=g);b=g}return b[a.shift()]= -c}function gb(b,a,c){if(!a)return b;for(var a=a.split("."),d,e=b,g=a.length,h=0;h7),hasEvent:function(c){if(c=="input"&&Z==9)return!1;if(w(a[c])){var e=b.document.createElement("div");a[c]="on"+c in e}return a[c]},csp:!1}}]}function Vc(){this.$get=I(P)}function Ob(b){var a={},c,d,e;if(!b)return a;m(b.split("\n"),function(b){e=b.indexOf(":");c=z(Q(b.substr(0, -e)));d=Q(b.substr(e+1));c&&(a[c]?a[c]+=", "+d:a[c]=d)});return a}function Pb(b){var a=L(b)?b:q;return function(c){a||(a=Ob(b));return c?a[z(c)]||null:a}}function Qb(b,a,c){if(H(c))return c(b,a);m(c,function(c){b=c(b,a)});return b}function Wc(){var b=/^\s*(\[|\{[^\{])/,a=/[\}\]]\s*$/,c=/^\)\]\}',?\n/,d=this.defaults={transformResponse:[function(d){B(d)&&(d=d.replace(c,""),b.test(d)&&a.test(d)&&(d=pb(d,!0)));return d}],transformRequest:[function(a){return L(a)&&wa.apply(a)!=="[object File]"?da(a):a}], -headers:{common:{Accept:"application/json, text/plain, */*","X-Requested-With":"XMLHttpRequest"},post:{"Content-Type":"application/json;charset=utf-8"},put:{"Content-Type":"application/json;charset=utf-8"}}},e=this.responseInterceptors=[];this.$get=["$httpBackend","$browser","$cacheFactory","$rootScope","$q","$injector",function(a,b,c,i,j,k){function l(a){function c(a){var b=v({},a,{data:Qb(a.data,a.headers,f)});return 200<=a.status&&a.status<300?b:j.reject(b)}a.method=la(a.method);var e=a.transformRequest|| -d.transformRequest,f=a.transformResponse||d.transformResponse,g=d.headers,g=v({"X-XSRF-TOKEN":b.cookies()["XSRF-TOKEN"]},g.common,g[z(a.method)],a.headers),e=Qb(a.data,Pb(g),e),i;w(a.data)&&delete g["Content-Type"];i=n(a,e,g);i=i.then(c,c);m(s,function(a){i=a(i)});i.success=function(b){i.then(function(c){b(c.data,c.status,c.headers,a)});return i};i.error=function(b){i.then(null,function(c){b(c.data,c.status,c.headers,a)});return i};return i}function n(b,c,d){function e(a,b,c){m&&(200<=a&&a<300?m.put(q, -[a,b,Ob(c)]):m.remove(q));f(b,a,c);i.$apply()}function f(a,c,d){c=Math.max(c,0);(200<=c&&c<300?k.resolve:k.reject)({data:a,status:c,headers:Pb(d),config:b})}function h(){var a=za(l.pendingRequests,b);a!==-1&&l.pendingRequests.splice(a,1)}var k=j.defer(),n=k.promise,m,s,q=o(b.url,b.params);l.pendingRequests.push(b);n.then(h,h);b.cache&&b.method=="GET"&&(m=L(b.cache)?b.cache:p);if(m)if(s=m.get(q))if(s.then)return s.then(h,h),s;else E(s)?f(s[1],s[0],U(s[2])):f(s,200,{});else m.put(q,n);s||a(b.method, -q,c,e,d,b.timeout,b.withCredentials);return n}function o(a,b){if(!b)return a;var c=[];fc(b,function(a,b){a==null||a==q||(L(a)&&(a=da(a)),c.push(encodeURIComponent(b)+"="+encodeURIComponent(a)))});return a+(a.indexOf("?")==-1?"?":"&")+c.join("&")}var p=c("$http"),s=[];m(e,function(a){s.push(B(a)?k.get(a):k.invoke(a))});l.pendingRequests=[];(function(a){m(arguments,function(a){l[a]=function(b,c){return l(v(c||{},{method:a,url:b}))}})})("get","delete","head","jsonp");(function(a){m(arguments,function(a){l[a]= -function(b,c,d){return l(v(d||{},{method:a,url:b,data:c}))}})})("post","put");l.defaults=d;return l}]}function Xc(){this.$get=["$browser","$window","$document",function(b,a,c){return Yc(b,Zc,b.defer,a.angular.callbacks,c[0],a.location.protocol.replace(":",""))}]}function Yc(b,a,c,d,e,g){function h(a,b){var c=e.createElement("script"),d=function(){e.body.removeChild(c);b&&b()};c.type="text/javascript";c.src=a;Z?c.onreadystatechange=function(){/loaded|complete/.test(c.readyState)&&d()}:c.onload=c.onerror= -d;e.body.appendChild(c)}return function(e,i,j,k,l,n,o){function p(a,c,d,e){c=(i.match(Hb)||["",g])[1]=="file"?d?200:404:c;a(c==1223?204:c,d,e);b.$$completeOutstandingRequest(C)}b.$$incOutstandingRequestCount();i=i||b.url();if(z(e)=="jsonp"){var s="_"+(d.counter++).toString(36);d[s]=function(a){d[s].data=a};h(i.replace("JSON_CALLBACK","angular.callbacks."+s),function(){d[s].data?p(k,200,d[s].data):p(k,-2);delete d[s]})}else{var t=new a;t.open(e,i,!0);m(l,function(a,b){a&&t.setRequestHeader(b,a)}); -var q;t.onreadystatechange=function(){if(t.readyState==4){var a=t.getAllResponseHeaders(),b=["Cache-Control","Content-Language","Content-Type","Expires","Last-Modified","Pragma"];a||(a="",m(b,function(b){var c=t.getResponseHeader(b);c&&(a+=b+": "+c+"\n")}));p(k,q||t.status,t.responseText,a)}};if(o)t.withCredentials=!0;t.send(j||"");n>0&&c(function(){q=-1;t.abort()},n)}}}function $c(){this.$get=function(){return{id:"en-us",NUMBER_FORMATS:{DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{minInt:1,minFrac:0, -maxFrac:3,posPre:"",posSuf:"",negPre:"-",negSuf:"",gSize:3,lgSize:3},{minInt:1,minFrac:2,maxFrac:2,posPre:"\u00a4",posSuf:"",negPre:"(\u00a4",negSuf:")",gSize:3,lgSize:3}],CURRENCY_SYM:"$"},DATETIME_FORMATS:{MONTH:"January,February,March,April,May,June,July,August,September,October,November,December".split(","),SHORTMONTH:"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec".split(","),DAY:"Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday".split(","),SHORTDAY:"Sun,Mon,Tue,Wed,Thu,Fri,Sat".split(","), -AMPMS:["AM","PM"],medium:"MMM d, y h:mm:ss a","short":"M/d/yy h:mm a",fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",mediumDate:"MMM d, y",shortDate:"M/d/yy",mediumTime:"h:mm:ss a",shortTime:"h:mm a"},pluralCat:function(b){return b===1?"one":"other"}}}}function ad(){this.$get=["$rootScope","$browser","$q","$exceptionHandler",function(b,a,c,d){function e(e,f,i){var j=c.defer(),k=j.promise,l=y(i)&&!i,f=a.defer(function(){try{j.resolve(e())}catch(a){j.reject(a),d(a)}l||b.$apply()},f),i=function(){delete g[k.$$timeoutId]}; -k.$$timeoutId=f;g[f]=j;k.then(i,i);return k}var g={};e.cancel=function(b){return b&&b.$$timeoutId in g?(g[b.$$timeoutId].reject("canceled"),a.defer.cancel(b.$$timeoutId)):!1};return e}]}function Rb(b){function a(a,e){return b.factory(a+c,e)}var c="Filter";this.register=a;this.$get=["$injector",function(a){return function(b){return a.get(b+c)}}];a("currency",Sb);a("date",Tb);a("filter",bd);a("json",cd);a("limitTo",dd);a("lowercase",ed);a("number",Ub);a("orderBy",Vb);a("uppercase",fd)}function bd(){return function(b, -a){if(!E(b))return b;var c=[];c.check=function(a){for(var b=0;b-1;case "object":for(var c in a)if(c.charAt(0)!=="$"&&d(a[c],b))return!0;return!1;case "array":for(c=0;ce+1?h="0":(f=h,j=!0)}if(!j){h=(h.split(Xb)[1]||"").length;w(e)&&(e=Math.min(Math.max(a.minFrac,h),a.maxFrac));var h=Math.pow(10,e),b=Math.round(b*h)/h,b=(""+b).split(Xb),h=b[0],b=b[1]||"",j=0,k=a.lgSize, -l=a.gSize;if(h.length>=k+l)for(var j=h.length-k,n=0;n0||e> --c)e+=c;e===0&&c==-12&&(e=12);return jb(e,a,d)}}function Ja(b,a){return function(c,d){var e=c["get"+b](),g=la(a?"SHORT"+b:b);return d[g][e]}}function Tb(b){function a(a){var b;if(b=a.match(c)){var a=new Date(0),g=0,h=0;b[9]&&(g=G(b[9]+b[10]),h=G(b[9]+b[11]));a.setUTCFullYear(G(b[1]),G(b[2])-1,G(b[3]));a.setUTCHours(G(b[4]||0)-g,G(b[5]||0)-h,G(b[6]||0),G(b[7]||0))}return a}var c=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;return function(c, -e){var g="",h=[],f,i,e=e||"mediumDate",e=b.DATETIME_FORMATS[e]||e;B(c)&&(c=gd.test(c)?G(c):a(c));Qa(c)&&(c=new Date(c));if(!na(c))return c;for(;e;)(i=hd.exec(e))?(h=h.concat(ha.call(i,1)),e=h.pop()):(h.push(e),e=null);m(h,function(a){f=id[a];g+=f?f(c,b.DATETIME_FORMATS):a.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function cd(){return function(b){return da(b,!0)}}function dd(){return function(b,a){if(!(b instanceof Array))return b;var a=G(a),c=[],d,e;if(!b||!(b instanceof Array))return c; -a>b.length?a=b.length:a<-b.length&&(a=-b.length);a>0?(d=0,e=a):(d=b.length+a,e=b.length);for(;dn?(d.$setValidity("maxlength",!1),q):(d.$setValidity("maxlength",!0),a)};d.$parsers.push(c);d.$formatters.push(c)}}function kb(b,a){b="ngClass"+b;return S(function(c,d,e){function g(b){if(a===!0||c.$index%2===a)i&&!fa(b,i)&&h(i),f(b);i=U(b)}function h(a){L(a)&& -!E(a)&&(a=Ra(a,function(a,b){if(a)return b}));d.removeClass(E(a)?a.join(" "):a)}function f(a){L(a)&&!E(a)&&(a=Ra(a,function(a,b){if(a)return b}));a&&d.addClass(E(a)?a.join(" "):a)}var i=q;c.$watch(e[b],g,!0);e.$observe("class",function(){var a=c.$eval(e[b]);g(a,a)});b!=="ngClass"&&c.$watch("$index",function(d,g){var i=d&1;i!==g&1&&(i===a?f(c.$eval(e[b])):h(c.$eval(e[b])))})})}var z=function(b){return B(b)?b.toLowerCase():b},la=function(b){return B(b)?b.toUpperCase():b},Z=G((/msie (\d+)/.exec(z(navigator.userAgent))|| -[])[1]),u,ca,ha=[].slice,Pa=[].push,wa=Object.prototype.toString,Ya=P.angular||(P.angular={}),sa,fb,aa=["0","0","0"];C.$inject=[];ma.$inject=[];fb=Z<9?function(b){b=b.nodeName?b:b[0];return b.scopeName&&b.scopeName!="HTML"?la(b.scopeName+":"+b.nodeName):b.nodeName}:function(b){return b.nodeName?b.nodeName:b[0].nodeName};var kc=/[A-Z]/g,jd={full:"1.0.7",major:1,minor:0,dot:7,codeName:"monochromatic-rainbow"},Ba=K.cache={},Aa=K.expando="ng-"+(new Date).getTime(),oc=1,$b=P.document.addEventListener? -function(b,a,c){b.addEventListener(a,c,!1)}:function(b,a,c){b.attachEvent("on"+a,c)},db=P.document.removeEventListener?function(b,a,c){b.removeEventListener(a,c,!1)}:function(b,a,c){b.detachEvent("on"+a,c)},mc=/([\:\-\_]+(.))/g,nc=/^moz([A-Z])/,ua=K.prototype={ready:function(b){function a(){c||(c=!0,b())}var c=!1;this.bind("DOMContentLoaded",a);K(P).bind("load",a)},toString:function(){var b=[];m(this,function(a){b.push(""+a)});return"["+b.join(", ")+"]"},eq:function(b){return b>=0?u(this[b]):u(this[this.length+ -b])},length:0,push:Pa,sort:[].sort,splice:[].splice},Ea={};m("multiple,selected,checked,disabled,readOnly,required".split(","),function(b){Ea[z(b)]=b});var Bb={};m("input,select,option,textarea,button,form".split(","),function(b){Bb[la(b)]=!0});m({data:wb,inheritedData:Da,scope:function(b){return Da(b,"$scope")},controller:zb,injector:function(b){return Da(b,"$injector")},removeAttr:function(b,a){b.removeAttribute(a)},hasClass:Ca,css:function(b,a,c){a=tb(a);if(y(c))b.style[a]=c;else{var d;Z<=8&&(d= -b.currentStyle&&b.currentStyle[a],d===""&&(d="auto"));d=d||b.style[a];Z<=8&&(d=d===""?q:d);return d}},attr:function(b,a,c){var d=z(a);if(Ea[d])if(y(c))c?(b[a]=!0,b.setAttribute(a,d)):(b[a]=!1,b.removeAttribute(d));else return b[a]||(b.attributes.getNamedItem(a)||C).specified?d:q;else if(y(c))b.setAttribute(a,c);else if(b.getAttribute)return b=b.getAttribute(a,2),b===null?q:b},prop:function(b,a,c){if(y(c))b[a]=c;else return b[a]},text:v(Z<9?function(b,a){if(b.nodeType==1){if(w(a))return b.innerText; -b.innerText=a}else{if(w(a))return b.nodeValue;b.nodeValue=a}}:function(b,a){if(w(a))return b.textContent;b.textContent=a},{$dv:""}),val:function(b,a){if(w(a))return b.value;b.value=a},html:function(b,a){if(w(a))return b.innerHTML;for(var c=0,d=b.childNodes;c":function(a,c,d,e){return d(a,c)>e(a,c)},"<=":function(a,c,d,e){return d(a,c)<=e(a,c)},">=":function(a,c,d,e){return d(a,c)>=e(a,c)},"&&":function(a,c,d,e){return d(a,c)&&e(a,c)},"||":function(a,c,d,e){return d(a,c)||e(a,c)},"&":function(a,c,d,e){return d(a,c)&e(a,c)},"|":function(a,c,d,e){return e(a,c)(a,c,d(a,c))},"!":function(a,c,d){return!d(a,c)}},Mc={n:"\n",f:"\u000c",r:"\r",t:"\t",v:"\u000b","'":"'",'"':'"'},ib={},Zc=P.XMLHttpRequest||function(){try{return new ActiveXObject("Msxml2.XMLHTTP.6.0")}catch(a){}try{return new ActiveXObject("Msxml2.XMLHTTP.3.0")}catch(c){}try{return new ActiveXObject("Msxml2.XMLHTTP")}catch(d){}throw Error("This browser does not support XMLHttpRequest."); -};Rb.$inject=["$provide"];Sb.$inject=["$locale"];Ub.$inject=["$locale"];var Xb=".",id={yyyy:O("FullYear",4),yy:O("FullYear",2,0,!0),y:O("FullYear",1),MMMM:Ja("Month"),MMM:Ja("Month",!0),MM:O("Month",2,1),M:O("Month",1,1),dd:O("Date",2),d:O("Date",1),HH:O("Hours",2),H:O("Hours",1),hh:O("Hours",2,-12),h:O("Hours",1,-12),mm:O("Minutes",2),m:O("Minutes",1),ss:O("Seconds",2),s:O("Seconds",1),EEEE:Ja("Day"),EEE:Ja("Day",!0),a:function(a,c){return a.getHours()<12?c.AMPMS[0]:c.AMPMS[1]},Z:function(a){var a= --1*a.getTimezoneOffset(),c=a>=0?"+":"";c+=jb(Math[a>0?"floor":"ceil"](a/60),2)+jb(Math.abs(a%60),2);return c}},hd=/((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/,gd=/^\d+$/;Tb.$inject=["$locale"];var ed=I(z),fd=I(la);Vb.$inject=["$parse"];var kd=I({restrict:"E",compile:function(a,c){Z<=8&&(!c.href&&!c.name&&c.$set("href",""),a.append(T.createComment("IE fix")));return function(a,c){c.bind("click",function(a){c.attr("href")||a.preventDefault()})}}}),lb={};m(Ea,function(a, -c){var d=ea("ng-"+c);lb[d]=function(){return{priority:100,compile:function(){return function(a,g,h){a.$watch(h[d],function(a){h.$set(c,!!a)})}}}}});m(["src","href"],function(a){var c=ea("ng-"+a);lb[c]=function(){return{priority:99,link:function(d,e,g){g.$observe(c,function(c){c&&(g.$set(a,c),Z&&e.prop(a,g[a]))})}}}});var Ma={$addControl:C,$removeControl:C,$setValidity:C,$setDirty:C};Yb.$inject=["$element","$attrs","$scope"];var Pa=function(a){return["$timeout",function(c){var d={name:"form",restrict:"E", -controller:Yb,compile:function(){return{pre:function(a,d,h,f){if(!h.action){var i=function(a){a.preventDefault?a.preventDefault():a.returnValue=!1};$b(d[0],"submit",i);d.bind("$destroy",function(){c(function(){db(d[0],"submit",i)},0,!1)})}var j=d.parent().controller("form"),k=h.name||h.ngForm;k&&(a[k]=f);j&&d.bind("$destroy",function(){j.$removeControl(f);k&&(a[k]=q);v(f,Ma)})}}}};return a?v(U(d),{restrict:"EAC"}):d}]},ld=Pa(),md=Pa(!0),nd=/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/, -od=/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/,pd=/^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/,bc={text:Oa,number:function(a,c,d,e,g,h){Oa(a,c,d,e,g,h);e.$parsers.push(function(a){var c=X(a);return c||pd.test(a)?(e.$setValidity("number",!0),a===""?null:c?a:parseFloat(a)):(e.$setValidity("number",!1),q)});e.$formatters.push(function(a){return X(a)?"":""+a});if(d.min){var f=parseFloat(d.min),a=function(a){return!X(a)&&ai?(e.$setValidity("max",!1),q):(e.$setValidity("max",!0),a)};e.$parsers.push(d);e.$formatters.push(d)}e.$formatters.push(function(a){return X(a)||Qa(a)?(e.$setValidity("number",!0),a):(e.$setValidity("number",!1),q)})},url:function(a,c,d,e,g,h){Oa(a,c,d,e,g,h);a=function(a){return X(a)||nd.test(a)?(e.$setValidity("url",!0),a):(e.$setValidity("url",!1),q)};e.$formatters.push(a);e.$parsers.push(a)},email:function(a, -c,d,e,g,h){Oa(a,c,d,e,g,h);a=function(a){return X(a)||od.test(a)?(e.$setValidity("email",!0),a):(e.$setValidity("email",!1),q)};e.$formatters.push(a);e.$parsers.push(a)},radio:function(a,c,d,e){w(d.name)&&c.attr("name",xa());c.bind("click",function(){c[0].checked&&a.$apply(function(){e.$setViewValue(d.value)})});e.$render=function(){c[0].checked=d.value==e.$viewValue};d.$observe("value",e.$render)},checkbox:function(a,c,d,e){var g=d.ngTrueValue,h=d.ngFalseValue;B(g)||(g=!0);B(h)||(h=!1);c.bind("click", -function(){a.$apply(function(){e.$setViewValue(c[0].checked)})});e.$render=function(){c[0].checked=e.$viewValue};e.$formatters.push(function(a){return a===g});e.$parsers.push(function(a){return a?g:h})},hidden:C,button:C,submit:C,reset:C},cc=["$browser","$sniffer",function(a,c){return{restrict:"E",require:"?ngModel",link:function(d,e,g,h){h&&(bc[z(g.type)]||bc.text)(d,e,g,h,c,a)}}}],La="ng-valid",Ka="ng-invalid",Na="ng-pristine",Zb="ng-dirty",qd=["$scope","$exceptionHandler","$attrs","$element","$parse", -function(a,c,d,e,g){function h(a,c){c=c?"-"+Za(c,"-"):"";e.removeClass((a?Ka:La)+c).addClass((a?La:Ka)+c)}this.$modelValue=this.$viewValue=Number.NaN;this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=[];this.$pristine=!0;this.$dirty=!1;this.$valid=!0;this.$invalid=!1;this.$name=d.name;var f=g(d.ngModel),i=f.assign;if(!i)throw Error(Eb+d.ngModel+" ("+pa(e)+")");this.$render=C;var j=e.inheritedData("$formController")||Ma,k=0,l=this.$error={};e.addClass(Na);h(!0);this.$setValidity=function(a, -c){if(l[a]!==!c){if(c){if(l[a]&&k--,!k)h(!0),this.$valid=!0,this.$invalid=!1}else h(!1),this.$invalid=!0,this.$valid=!1,k++;l[a]=!c;h(c,a);j.$setValidity(a,c,this)}};this.$setViewValue=function(d){this.$viewValue=d;if(this.$pristine)this.$dirty=!0,this.$pristine=!1,e.removeClass(Na).addClass(Zb),j.$setDirty();m(this.$parsers,function(a){d=a(d)});if(this.$modelValue!==d)this.$modelValue=d,i(a,d),m(this.$viewChangeListeners,function(a){try{a()}catch(d){c(d)}})};var n=this;a.$watch(function(){var c= -f(a);if(n.$modelValue!==c){var d=n.$formatters,e=d.length;for(n.$modelValue=c;e--;)c=d[e](c);if(n.$viewValue!==c)n.$viewValue=c,n.$render()}})}],rd=function(){return{require:["ngModel","^?form"],controller:qd,link:function(a,c,d,e){var g=e[0],h=e[1]||Ma;h.$addControl(g);c.bind("$destroy",function(){h.$removeControl(g)})}}},sd=I({require:"ngModel",link:function(a,c,d,e){e.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),dc=function(){return{require:"?ngModel",link:function(a,c,d,e){if(e){d.required= -!0;var g=function(a){if(d.required&&(X(a)||a===!1))e.$setValidity("required",!1);else return e.$setValidity("required",!0),a};e.$formatters.push(g);e.$parsers.unshift(g);d.$observe("required",function(){g(e.$viewValue)})}}}},td=function(){return{require:"ngModel",link:function(a,c,d,e){var g=(a=/\/(.*)\//.exec(d.ngList))&&RegExp(a[1])||d.ngList||",";e.$parsers.push(function(a){var c=[];a&&m(a.split(g),function(a){a&&c.push(Q(a))});return c});e.$formatters.push(function(a){return E(a)?a.join(", "): -q})}}},ud=/^(true|false|\d+)$/,vd=function(){return{priority:100,compile:function(a,c){return ud.test(c.ngValue)?function(a,c,g){g.$set("value",a.$eval(g.ngValue))}:function(a,c,g){a.$watch(g.ngValue,function(a){g.$set("value",a,!1)})}}}},wd=S(function(a,c,d){c.addClass("ng-binding").data("$binding",d.ngBind);a.$watch(d.ngBind,function(a){c.text(a==q?"":a)})}),xd=["$interpolate",function(a){return function(c,d,e){c=a(d.attr(e.$attr.ngBindTemplate));d.addClass("ng-binding").data("$binding",c);e.$observe("ngBindTemplate", -function(a){d.text(a)})}}],yd=[function(){return function(a,c,d){c.addClass("ng-binding").data("$binding",d.ngBindHtmlUnsafe);a.$watch(d.ngBindHtmlUnsafe,function(a){c.html(a||"")})}}],zd=kb("",!0),Ad=kb("Odd",0),Bd=kb("Even",1),Cd=S({compile:function(a,c){c.$set("ngCloak",q);a.removeClass("ng-cloak")}}),Dd=[function(){return{scope:!0,controller:"@"}}],Ed=["$sniffer",function(a){return{priority:1E3,compile:function(){a.csp=!0}}}],ec={};m("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave".split(" "), -function(a){var c=ea("ng-"+a);ec[c]=["$parse",function(d){return function(e,g,h){var f=d(h[c]);g.bind(z(a),function(a){e.$apply(function(){f(e,{$event:a})})})}}]});var Fd=S(function(a,c,d){c.bind("submit",function(){a.$apply(d.ngSubmit)})}),Gd=["$http","$templateCache","$anchorScroll","$compile",function(a,c,d,e){return{restrict:"ECA",terminal:!0,compile:function(g,h){var f=h.ngInclude||h.src,i=h.onload||"",j=h.autoscroll;return function(g,h){var n=0,o,p=function(){o&&(o.$destroy(),o=null);h.html("")}; -g.$watch(f,function(f){var m=++n;f?a.get(f,{cache:c}).success(function(a){m===n&&(o&&o.$destroy(),o=g.$new(),h.html(a),e(h.contents())(o),y(j)&&(!j||g.$eval(j))&&d(),o.$emit("$includeContentLoaded"),g.$eval(i))}).error(function(){m===n&&p()}):p()})}}}}],Hd=S({compile:function(){return{pre:function(a,c,d){a.$eval(d.ngInit)}}}}),Id=S({terminal:!0,priority:1E3}),Jd=["$locale","$interpolate",function(a,c){var d=/{}/g;return{restrict:"EA",link:function(e,g,h){var f=h.count,i=g.attr(h.$attr.when),j=h.offset|| -0,k=e.$eval(i),l={},n=c.startSymbol(),o=c.endSymbol();m(k,function(a,e){l[e]=c(a.replace(d,n+f+"-"+j+o))});e.$watch(function(){var c=parseFloat(e.$eval(f));return isNaN(c)?"":(c in k||(c=a.pluralCat(c-j)),l[c](e,g,!0))},function(a){g.text(a)})}}}],Kd=S({transclude:"element",priority:1E3,terminal:!0,compile:function(a,c,d){return function(a,c,h){var f=h.ngRepeat,h=f.match(/^\s*(.+)\s+in\s+(.*)\s*$/),i,j,k;if(!h)throw Error("Expected ngRepeat in form of '_item_ in _collection_' but got '"+f+"'.");f= -h[1];i=h[2];h=f.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);if(!h)throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '"+f+"'.");j=h[3]||h[1];k=h[2];var l=new eb;a.$watch(function(a){var e,f,h=a.$eval(i),m=c,q=new eb,y,A,u,w,r,v;if(E(h))r=h||[];else{r=[];for(u in h)h.hasOwnProperty(u)&&u.charAt(0)!="$"&&r.push(u);r.sort()}y=r.length-1;e=0;for(f=r.length;ez;)u.pop().element.remove()}for(;r.length> -x;)r.pop()[0].element.remove()}var i;if(!(i=s.match(d)))throw Error("Expected ngOptions in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got '"+s+"'.");var j=c(i[2]||i[1]),k=i[4]||i[6],l=i[5],m=c(i[3]||""),n=c(i[2]?i[1]:k),o=c(i[7]),r=[[{element:f,label:""}]];t&&(a(t)(e),t.removeClass("ng-scope"),t.remove());f.html("");f.bind("change",function(){e.$apply(function(){var a,c=o(e)||[],d={},h,i,j,m,s,t;if(p){i=[];m=0;for(t=r.length;m@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak{display:none;}ng\\:form{display:block;}'); +(function(W,X,u){'use strict';function z(b){return function(){var a=arguments[0],c,a="["+(b?b+":":"")+a+"] http://errors.angularjs.org/1.2.27/"+(b?b+"/":"")+a;for(c=1;c").append(b).html();try{return 3===b[0].nodeType?x(c):c.match(/^(<[^>]+>)/)[1].replace(/^<([\w\-]+)/,function(a,b){return"<"+x(b)})}catch(d){return x(c)}}function bc(b){try{return decodeURIComponent(b)}catch(a){}}function cc(b){var a={},c,d;r((b||"").split("&"),function(b){b&&(c=b.replace(/\+/g,"%20").split("="),d=bc(c[0]),D(d)&&(b=D(c[1])?bc(c[1]):!0,lb.call(a,d)?L(a[d])?a[d].push(b):a[d]=[a[d],b]:a[d]=b))});return a}function Cb(b){var a= +[];r(b,function(b,d){L(b)?r(b,function(b){a.push(Da(d,!0)+(!0===b?"":"="+Da(b,!0)))}):a.push(Da(d,!0)+(!0===b?"":"="+Da(b,!0)))});return a.length?a.join("&"):""}function mb(b){return Da(b,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function Da(b,a){return encodeURIComponent(b).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,a?"%20":"+")}function Wc(b,a){function c(a){a&&d.push(a)}var d=[b],e,f,g=["ng:app","ng-app","x-ng-app", +"data-ng-app"],h=/\sng[:\-]app(:\s*([\w\d_]+);?)?\s/;r(g,function(a){g[a]=!0;c(X.getElementById(a));a=a.replace(":","\\:");b.querySelectorAll&&(r(b.querySelectorAll("."+a),c),r(b.querySelectorAll("."+a+"\\:"),c),r(b.querySelectorAll("["+a+"]"),c))});r(d,function(a){if(!e){var b=h.exec(" "+a.className+" ");b?(e=a,f=(b[2]||"").replace(/\s+/g,",")):r(a.attributes,function(b){!e&&g[b.name]&&(e=a,f=b.value)})}});e&&a(e,f?[f]:[])}function dc(b,a){var c=function(){b=A(b);if(b.injector()){var c=b[0]===X? +"document":ia(b);throw Va("btstrpd",c.replace(//,">"));}a=a||[];a.unshift(["$provide",function(a){a.value("$rootElement",b)}]);a.unshift("ng");c=ec(a);c.invoke(["$rootScope","$rootElement","$compile","$injector","$animate",function(a,b,c,d,e){a.$apply(function(){b.data("$injector",d);c(b)(a)})}]);return c},d=/^NG_DEFER_BOOTSTRAP!/;if(W&&!d.test(W.name))return c();W.name=W.name.replace(d,"");Xa.resumeBootstrap=function(b){r(b,function(b){a.push(b)});c()}}function nb(b,a){a= +a||"_";return b.replace(Xc,function(b,d){return(d?a:"")+b.toLowerCase()})}function Db(b,a,c){if(!b)throw Va("areq",a||"?",c||"required");return b}function Ya(b,a,c){c&&L(b)&&(b=b[b.length-1]);Db(N(b),a,"not a function, got "+(b&&"object"===typeof b?b.constructor.name||"Object":typeof b));return b}function Ea(b,a){if("hasOwnProperty"===b)throw Va("badname",a);}function fc(b,a,c){if(!a)return b;a=a.split(".");for(var d,e=b,f=a.length,g=0;g 
"+e[1]+a.replace(le,"<$1>")+e[2];d.removeChild(d.firstChild);for(a=e[0];a--;)d=d.lastChild;a=0;for(e=d.childNodes.length;a=R?(c.preventDefault=null,c.stopPropagation=null,c.isDefaultPrevented=null):(delete c.preventDefault,delete c.stopPropagation,delete c.isDefaultPrevented)};c.elem=b;return c}function Na(b,a){var c=typeof b,d;"function"==c||"object"==c&&null!==b?"function"==typeof(d= +b.$$hashKey)?d=b.$$hashKey():d===u&&(d=b.$$hashKey=(a||ib)()):d=b;return c+":"+d}function db(b,a){if(a){var c=0;this.nextUid=function(){return++c}}r(b,this.put,this)}function qc(b){var a,c;"function"===typeof b?(a=b.$inject)||(a=[],b.length&&(c=b.toString().replace(oe,""),c=c.match(pe),r(c[1].split(qe),function(b){b.replace(re,function(b,c,d){a.push(d)})})),b.$inject=a):L(b)?(c=b.length-1,Ya(b[c],"fn"),a=b.slice(0,c)):Ya(b,"fn",!0);return a}function ec(b){function a(a){return function(b,c){if(T(b))r(b, +Yb(a));else return a(b,c)}}function c(a,b){Ea(a,"service");if(N(b)||L(b))b=n.instantiate(b);if(!b.$get)throw eb("pget",a);return l[a+h]=b}function d(a,b){return c(a,{$get:b})}function e(a){var b=[],c,d,f,h;r(a,function(a){if(!m.get(a)){m.put(a,!0);try{if(G(a))for(c=$a(a),b=b.concat(e(c.requires)).concat(c._runBlocks),d=c._invokeQueue,f=0,h=d.length;f 4096 bytes)!"));else{if(k.cookie!== +ca)for(ca=k.cookie,d=ca.split("; "),M={},f=0;fm&&this.remove(q.key),b},get:function(a){if(m").parent()[0])});var f=O(a,b,a,c,d,e);ba(a,"ng-scope");return function(b,c,d,e){Db(b,"scope");var g=c?Oa.clone.call(a):a;r(d,function(a,b){g.data("$"+b+"Controller",a)});d=0;for(var k=g.length;darguments.length&& +(b=a,a=u);Ia&&(c=ca);return n(a,b,c)}var y,Q,B,M,C,P,ca={},ra;y=c===f?d:ha(d,new Ob(A(f),d.$attr));Q=y.$$element;if(K){var ue=/^\s*([@=&])(\??)\s*(\w*)\s*$/;P=e.$new(!0);!I||I!==K&&I!==K.$$originalDirective?Q.data("$isolateScopeNoTemplate",P):Q.data("$isolateScope",P);ba(Q,"ng-isolate-scope");r(K.scope,function(a,c){var d=a.match(ue)||[],f=d[3]||c,g="?"==d[2],d=d[1],k,l,n,q;P.$$isolateBindings[c]=d+f;switch(d){case "@":y.$observe(f,function(a){P[c]=a});y.$$observers[f].$$scope=e;y[f]&&(P[c]=b(y[f])(e)); +break;case "=":if(g&&!y[f])break;l=p(y[f]);q=l.literal?Ca:function(a,b){return a===b||a!==a&&b!==b};n=l.assign||function(){k=P[c]=l(e);throw ja("nonassign",y[f],K.name);};k=P[c]=l(e);P.$watch(function(){var a=l(e);q(a,P[c])||(q(a,k)?n(e,a=P[c]):P[c]=a);return k=a},null,l.literal);break;case "&":l=p(y[f]);P[c]=function(a){return l(e,a)};break;default:throw ja("iscp",K.name,c,a);}})}ra=n&&w;O&&r(O,function(a){var b={$scope:a===K||a.$$isolateScope?P:e,$element:Q,$attrs:y,$transclude:ra},c;C=a.controller; +"@"==C&&(C=y[a.name]);c=s(C,b);ca[a.name]=c;Ia||Q.data("$"+a.name+"Controller",c);a.controllerAs&&(b.$scope[a.controllerAs]=c)});g=0;for(B=k.length;gH.priority)break;if(V=H.scope)M=M||H,H.templateUrl||(fb("new/isolated scope",K,H,x),T(V)&&(K=H));z=H.name;!H.templateUrl&&H.controller&&(V=H.controller,O=O||{},fb("'"+z+"' controller",O[z],H,x),O[z]=H);if(V=H.transclude)F=!0,H.$$tlb|| +(fb("transclusion",ea,H,x),ea=H),"element"==V?(Ia=!0,y=H.priority,V=x,x=d.$$element=A(X.createComment(" "+z+": "+d[z]+" ")),c=x[0],ra(f,wa.call(V,0),c),S=B(V,e,y,g&&g.name,{nonTlbTranscludeDirective:ea})):(V=A(Kb(c)).contents(),x.empty(),S=B(V,e));if(H.template)if(E=!0,fb("template",I,H,x),I=H,V=N(H.template)?H.template(x,d):H.template,V=W(V),H.replace){g=H;V=Ib.test(V)?A($(V)):[];c=V[0];if(1!=V.length||1!==c.nodeType)throw ja("tplrt",z,"");ra(f,x,c);sa={$attr:{}};V=ca(c,[],sa);var Z=a.splice(Ha+ +1,a.length-(Ha+1));K&&D(V);a=a.concat(V).concat(Z);v(d,sa);sa=a.length}else x.html(V);if(H.templateUrl)E=!0,fb("template",I,H,x),I=H,H.replace&&(g=H),J=te(a.splice(Ha,a.length-Ha),x,d,f,F&&S,k,q,{controllerDirectives:O,newIsolateScopeDirective:K,templateDirective:I,nonTlbTranscludeDirective:ea}),sa=a.length;else if(H.compile)try{R=H.compile(x,d,S),N(R)?w(null,R,U,Y):R&&w(R.pre,R.post,U,Y)}catch(ve){l(ve,ia(x))}H.terminal&&(J.terminal=!0,y=Math.max(y,H.priority))}J.scope=M&&!0===M.scope;J.transcludeOnThisElement= +F;J.templateOnThisElement=E;J.transclude=S;n.hasElementTranscludeDirective=Ia;return J}function D(a){for(var b=0,c=a.length;bq.priority)&&-1!=q.restrict.indexOf(f)&&(m&&(q=$b(q,{$$start:m,$$end:n})),b.push(q),p=q)}catch(y){l(y)}}return p}function v(a,b){var c=b.$attr,d=a.$attr,e=a.$$element;r(a,function(d,e){"$"!= +e.charAt(0)&&(b[e]&&b[e]!==d&&(d+=("style"===e?";":" ")+b[e]),a.$set(e,d,!0,c[e]))});r(b,function(b,f){"class"==f?(ba(e,b),a["class"]=(a["class"]?a["class"]+" ":"")+b):"style"==f?(e.attr("style",e.attr("style")+";"+b),a.style=(a.style?a.style+";":"")+b):"$"==f.charAt(0)||a.hasOwnProperty(f)||(a[f]=b,d[f]=c[f])})}function te(a,b,c,d,e,f,g,k){var p=[],l,m,w=b[0],s=a.shift(),y=E({},s,{templateUrl:null,transclude:null,replace:null,$$originalDirective:s}),J=N(s.templateUrl)?s.templateUrl(b,c):s.templateUrl; +b.empty();n.get(t.getTrustedResourceUrl(J),{cache:q}).success(function(q){var n,t;q=W(q);if(s.replace){q=Ib.test(q)?A($(q)):[];n=q[0];if(1!=q.length||1!==n.nodeType)throw ja("tplrt",s.name,J);q={$attr:{}};ra(d,b,n);var B=ca(n,[],q);T(s.scope)&&D(B);a=B.concat(a);v(c,q)}else n=w,b.html(q);a.unshift(y);l=I(a,n,c,e,b,s,f,g,k);r(d,function(a,c){a==n&&(d[c]=b[0])});for(m=O(b[0].childNodes,e);p.length;){q=p.shift();t=p.shift();var K=p.shift(),C=p.shift(),B=b[0];if(t!==w){var P=t.className;k.hasElementTranscludeDirective&& +s.replace||(B=Kb(n));ra(K,A(t),B);ba(A(B),P)}t=l.transcludeOnThisElement?M(q,l.transclude,C):C;l(m,q,B,d,t)}p=null}).error(function(a,b,c,d){throw ja("tpload",d.url);});return function(a,b,c,d,e){a=e;p?(p.push(b),p.push(c),p.push(d),p.push(a)):(l.transcludeOnThisElement&&(a=M(b,l.transclude,e)),l(m,b,c,d,a))}}function F(a,b){var c=b.priority-a.priority;return 0!==c?c:a.name!==b.name?a.namea.status?d:n.reject(d)}var c={method:"get",transformRequest:e.transformRequest,transformResponse:e.transformResponse},d=function(a){var b=e.headers,c=E({},a.headers),d,f,b=E({},b.common,b[x(a.method)]); +a:for(d in b){a=x(d);for(f in c)if(x(f)===a)continue a;c[d]=b[d]}(function(a){var b;r(a,function(c,d){N(c)&&(b=c(),null!=b?a[d]=b:delete a[d])})})(c);return c}(a);E(c,a);c.headers=d;c.method=La(c.method);var f=[function(a){d=a.headers;var c=vc(a.data,uc(d),a.transformRequest);F(c)&&r(d,function(a,b){"content-type"===x(b)&&delete d[b]});F(a.withCredentials)&&!F(e.withCredentials)&&(a.withCredentials=e.withCredentials);return s(a,c,d).then(b,b)},u],g=n.when(c);for(r(t,function(a){(a.request||a.requestError)&& +f.unshift(a.request,a.requestError);(a.response||a.responseError)&&f.push(a.response,a.responseError)});f.length;){a=f.shift();var h=f.shift(),g=g.then(a,h)}g.success=function(a){g.then(function(b){a(b.data,b.status,b.headers,c)});return g};g.error=function(a){g.then(null,function(b){a(b.data,b.status,b.headers,c)});return g};return g}function s(c,f,g){function m(a,b,c,e){C&&(200<=a&&300>a?C.put(A,[a,b,tc(c),e]):C.remove(A));q(b,a,c,e);d.$$phase||d.$apply()}function q(a,b,d,e){b=Math.max(b,0);(200<= +b&&300>b?t.resolve:t.reject)({data:a,status:b,headers:uc(d),config:c,statusText:e})}function s(){var a=Ta(p.pendingRequests,c);-1!==a&&p.pendingRequests.splice(a,1)}var t=n.defer(),r=t.promise,C,I,A=J(c.url,c.params);p.pendingRequests.push(c);r.then(s,s);!c.cache&&!e.cache||(!1===c.cache||"GET"!==c.method&&"JSONP"!==c.method)||(C=T(c.cache)?c.cache:T(e.cache)?e.cache:w);if(C)if(I=C.get(A),D(I)){if(I&&N(I.then))return I.then(s,s),I;L(I)?q(I[1],I[0],ha(I[2]),I[3]):q(I,200,{},"OK")}else C.put(A,r);F(I)&& +((I=Pb(c.url)?b.cookies()[c.xsrfCookieName||e.xsrfCookieName]:u)&&(g[c.xsrfHeaderName||e.xsrfHeaderName]=I),a(c.method,A,f,m,g,c.timeout,c.withCredentials,c.responseType));return r}function J(a,b){if(!b)return a;var c=[];Sc(b,function(a,b){null===a||F(a)||(L(a)||(a=[a]),r(a,function(a){T(a)&&(a=va(a)?a.toISOString():oa(a));c.push(Da(b)+"="+Da(a))}))});0=R&&(!b.match(/^(get|post|head|put|delete|options)$/i)|| +!W.XMLHttpRequest))return new W.ActiveXObject("Microsoft.XMLHTTP");if(W.XMLHttpRequest)return new W.XMLHttpRequest;throw z("$httpBackend")("noxhr");}function Ud(){this.$get=["$browser","$window","$document",function(b,a,c){return ye(b,xe,b.defer,a.angular.callbacks,c[0])}]}function ye(b,a,c,d,e){function f(a,b,c){var f=e.createElement("script"),g=null;f.type="text/javascript";f.src=a;f.async=!0;g=function(a){bb(f,"load",g);bb(f,"error",g);e.body.removeChild(f);f=null;var h=-1,s="unknown";a&&("load"!== +a.type||d[b].called||(a={type:"error"}),s=a.type,h="error"===a.type?404:200);c&&c(h,s)};sb(f,"load",g);sb(f,"error",g);8>=R&&(f.onreadystatechange=function(){G(f.readyState)&&/loaded|complete/.test(f.readyState)&&(f.onreadystatechange=null,g({type:"load"}))});e.body.appendChild(f);return g}var g=-1;return function(e,k,m,l,n,q,p,s){function J(){t=g;K&&K();B&&B.abort()}function w(a,d,e,f,g){O&&c.cancel(O);K=B=null;0===d&&(d=e?200:"file"==xa(k).protocol?404:0);a(1223===d?204:d,e,f,g||"");b.$$completeOutstandingRequest(v)} +var t;b.$$incOutstandingRequestCount();k=k||b.url();if("jsonp"==x(e)){var y="_"+(d.counter++).toString(36);d[y]=function(a){d[y].data=a;d[y].called=!0};var K=f(k.replace("JSON_CALLBACK","angular.callbacks."+y),y,function(a,b){w(l,a,d[y].data,"",b);d[y]=v})}else{var B=a(e);B.open(e,k,!0);r(n,function(a,b){D(a)&&B.setRequestHeader(b,a)});B.onreadystatechange=function(){if(B&&4==B.readyState){var a=null,b=null,c="";t!==g&&(a=B.getAllResponseHeaders(),b="response"in B?B.response:B.responseText);t===g&& +10>R||(c=B.statusText);w(l,t||B.status,b,a,c)}};p&&(B.withCredentials=!0);if(s)try{B.responseType=s}catch(ba){if("json"!==s)throw ba;}B.send(m||null)}if(0=h&&(n.resolve(p),l(q.$$intervalId),delete e[q.$$intervalId]);s||b.$apply()},g);e[q.$$intervalId]=n;return q}var e={};d.cancel= +function(b){return b&&b.$$intervalId in e?(e[b.$$intervalId].reject("canceled"),a.clearInterval(b.$$intervalId),delete e[b.$$intervalId],!0):!1};return d}]}function ad(){this.$get=function(){return{id:"en-us",NUMBER_FORMATS:{DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{minInt:1,minFrac:0,maxFrac:3,posPre:"",posSuf:"",negPre:"-",negSuf:"",gSize:3,lgSize:3},{minInt:1,minFrac:2,maxFrac:2,posPre:"\u00a4",posSuf:"",negPre:"(\u00a4",negSuf:")",gSize:3,lgSize:3}],CURRENCY_SYM:"$"},DATETIME_FORMATS:{MONTH:"January February March April May June July August September October November December".split(" "), +SHORTMONTH:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),DAY:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),SHORTDAY:"Sun Mon Tue Wed Thu Fri Sat".split(" "),AMPMS:["AM","PM"],medium:"MMM d, y h:mm:ss a","short":"M/d/yy h:mm a",fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",mediumDate:"MMM d, y",shortDate:"M/d/yy",mediumTime:"h:mm:ss a",shortTime:"h:mm a"},pluralCat:function(b){return 1===b?"one":"other"}}}}function Qb(b){b=b.split("/");for(var a=b.length;a--;)b[a]= +mb(b[a]);return b.join("/")}function xc(b,a,c){b=xa(b,c);a.$$protocol=b.protocol;a.$$host=b.hostname;a.$$port=U(b.port)||ze[b.protocol]||null}function yc(b,a,c){var d="/"!==b.charAt(0);d&&(b="/"+b);b=xa(b,c);a.$$path=decodeURIComponent(d&&"/"===b.pathname.charAt(0)?b.pathname.substring(1):b.pathname);a.$$search=cc(b.search);a.$$hash=decodeURIComponent(b.hash);a.$$path&&"/"!=a.$$path.charAt(0)&&(a.$$path="/"+a.$$path)}function ta(b,a){if(0===a.indexOf(b))return a.substr(b.length)}function Ga(b){var a= +b.indexOf("#");return-1==a?b:b.substr(0,a)}function Rb(b){return b.substr(0,Ga(b).lastIndexOf("/")+1)}function zc(b,a){this.$$html5=!0;a=a||"";var c=Rb(b);xc(b,this,b);this.$$parse=function(a){var e=ta(c,a);if(!G(e))throw Sb("ipthprfx",a,c);yc(e,this,b);this.$$path||(this.$$path="/");this.$$compose()};this.$$compose=function(){var a=Cb(this.$$search),b=this.$$hash?"#"+mb(this.$$hash):"";this.$$url=Qb(this.$$path)+(a?"?"+a:"")+b;this.$$absUrl=c+this.$$url.substr(1)};this.$$parseLinkUrl=function(d, +e){var f,g;(f=ta(b,d))!==u?(g=f,g=(f=ta(a,f))!==u?c+(ta("/",f)||f):b+g):(f=ta(c,d))!==u?g=c+f:c==d+"/"&&(g=c);g&&this.$$parse(g);return!!g}}function Tb(b,a){var c=Rb(b);xc(b,this,b);this.$$parse=function(d){var e=ta(b,d)||ta(c,d),e="#"==e.charAt(0)?ta(a,e):this.$$html5?e:"";if(!G(e))throw Sb("ihshprfx",d,a);yc(e,this,b);d=this.$$path;var f=/^\/[A-Z]:(\/.*)/;0===e.indexOf(b)&&(e=e.replace(b,""));f.exec(e)||(d=(e=f.exec(d))?e[1]:d);this.$$path=d;this.$$compose()};this.$$compose=function(){var c=Cb(this.$$search), +e=this.$$hash?"#"+mb(this.$$hash):"";this.$$url=Qb(this.$$path)+(c?"?"+c:"")+e;this.$$absUrl=b+(this.$$url?a+this.$$url:"")};this.$$parseLinkUrl=function(a,c){return Ga(b)==Ga(a)?(this.$$parse(a),!0):!1}}function Ac(b,a){this.$$html5=!0;Tb.apply(this,arguments);var c=Rb(b);this.$$parseLinkUrl=function(d,e){var f,g;b==Ga(d)?f=d:(g=ta(c,d))?f=b+a+g:c===d+"/"&&(f=c);f&&this.$$parse(f);return!!f};this.$$compose=function(){var c=Cb(this.$$search),e=this.$$hash?"#"+mb(this.$$hash):"";this.$$url=Qb(this.$$path)+ +(c?"?"+c:"")+e;this.$$absUrl=b+a+this.$$url}}function tb(b){return function(){return this[b]}}function Bc(b,a){return function(c){if(F(c))return this[b];this[b]=a(c);this.$$compose();return this}}function Vd(){var b="",a=!1;this.hashPrefix=function(a){return D(a)?(b=a,this):b};this.html5Mode=function(b){return D(b)?(a=b,this):a};this.$get=["$rootScope","$browser","$sniffer","$rootElement",function(c,d,e,f){function g(a){c.$broadcast("$locationChangeSuccess",h.absUrl(),a)}var h,k=d.baseHref(),m=d.url(); +a?(k=m.substring(0,m.indexOf("/",m.indexOf("//")+2))+(k||"/"),e=e.history?zc:Ac):(k=Ga(m),e=Tb);h=new e(k,"#"+b);h.$$parseLinkUrl(m,m);var l=/^\s*(javascript|mailto):/i;f.on("click",function(a){if(!a.ctrlKey&&!a.metaKey&&2!=a.which){for(var b=A(a.target);"a"!==x(b[0].nodeName);)if(b[0]===f[0]||!(b=b.parent())[0])return;var e=b.prop("href"),g=b.attr("href")||b.attr("xlink:href");T(e)&&"[object SVGAnimatedString]"===e.toString()&&(e=xa(e.animVal).href);l.test(e)||(!e||(b.attr("target")||a.isDefaultPrevented())|| +!h.$$parseLinkUrl(e,g))||(a.preventDefault(),h.absUrl()!=d.url()&&(c.$apply(),W.angular["ff-684208-preventDefault"]=!0))}});h.absUrl()!=m&&d.url(h.absUrl(),!0);d.onUrlChange(function(a){h.absUrl()!=a&&(c.$evalAsync(function(){var b=h.absUrl();h.$$parse(a);c.$broadcast("$locationChangeStart",a,b).defaultPrevented?(h.$$parse(b),d.url(b)):g(b)}),c.$$phase||c.$digest())});var n=0;c.$watch(function(){var a=d.url(),b=h.$$replace;n&&a==h.absUrl()||(n++,c.$evalAsync(function(){c.$broadcast("$locationChangeStart", +h.absUrl(),a).defaultPrevented?h.$$parse(a):(d.url(h.absUrl(),b),g(a))}));h.$$replace=!1;return n});return h}]}function Wd(){var b=!0,a=this;this.debugEnabled=function(a){return D(a)?(b=a,this):b};this.$get=["$window",function(c){function d(a){a instanceof Error&&(a.stack?a=a.message&&-1===a.stack.indexOf(a.message)?"Error: "+a.message+"\n"+a.stack:a.stack:a.sourceURL&&(a=a.message+"\n"+a.sourceURL+":"+a.line));return a}function e(a){var b=c.console||{},e=b[a]||b.log||v;a=!1;try{a=!!e.apply}catch(k){}return a? +function(){var a=[];r(arguments,function(b){a.push(d(b))});return e.apply(b,a)}:function(a,b){e(a,null==b?"":b)}}return{log:e("log"),info:e("info"),warn:e("warn"),error:e("error"),debug:function(){var c=e("debug");return function(){b&&c.apply(a,arguments)}}()}}]}function ka(b,a){if("__defineGetter__"===b||"__defineSetter__"===b||"__lookupGetter__"===b||"__lookupSetter__"===b||"__proto__"===b)throw la("isecfld",a);return b}function ma(b,a){if(b){if(b.constructor===b)throw la("isecfn",a);if(b.document&& +b.location&&b.alert&&b.setInterval)throw la("isecwindow",a);if(b.children&&(b.nodeName||b.prop&&b.attr&&b.find))throw la("isecdom",a);if(b===Object)throw la("isecobj",a);}return b}function ub(b,a,c,d,e){ma(b,d);e=e||{};a=a.split(".");for(var f,g=0;1g?Cc(f[0],f[1],f[2],f[3],f[4],c,a):function(b,d){var e=0,h;do h=Cc(f[e++],f[e++],f[e++],f[e++],f[e++],c,a)(b,d),d=u,b=h;while(eb.length?a=b.length:a<-b.length&&(a=-b.length);0a||37<=a&&40>=a)||p()});if(e.hasEvent("paste"))a.on("paste cut",p)}a.on("change",n);d.$render=function(){a.val(d.$isEmpty(d.$viewValue)?"":d.$viewValue)};var s=c.ngPattern;s&&((e=s.match(/^\/(.*)\/([gim]*)$/))? +(s=RegExp(e[1],e[2]),e=function(a){return ua(d,"pattern",d.$isEmpty(a)||s.test(a),a)}):e=function(c){var e=b.$eval(s);if(!e||!e.test)throw z("ngPattern")("noregexp",s,e,ia(a));return ua(d,"pattern",d.$isEmpty(c)||e.test(c),c)},d.$formatters.push(e),d.$parsers.push(e));if(c.ngMinlength){var r=U(c.ngMinlength);e=function(a){return ua(d,"minlength",d.$isEmpty(a)||a.length>=r,a)};d.$parsers.push(e);d.$formatters.push(e)}if(c.ngMaxlength){var w=U(c.ngMaxlength);e=function(a){return ua(d,"maxlength",d.$isEmpty(a)|| +a.length<=w,a)};d.$parsers.push(e);d.$formatters.push(e)}}function Wb(b,a){b="ngClass"+b;return["$animate",function(c){function d(a,b){var c=[],d=0;a:for(;dR?function(b){b=b.nodeName?b:b[0];return b.scopeName&&"HTML"!=b.scopeName?La(b.scopeName+":"+b.nodeName):b.nodeName}:function(b){return b.nodeName?b.nodeName:b[0].nodeName};var Za=function(){if(D(Za.isActive_))return Za.isActive_;var b=!(!X.querySelector("[ng-csp]")&& +!X.querySelector("[data-ng-csp]"));if(!b)try{new Function("")}catch(a){b=!0}return Za.isActive_=b},Xc=/[A-Z]/g,$c={full:"1.2.27",major:1,minor:2,dot:27,codeName:"prime-factorization"};S.expando="ng339";var cb=S.cache={},me=1,sb=W.document.addEventListener?function(b,a,c){b.addEventListener(a,c,!1)}:function(b,a,c){b.attachEvent("on"+a,c)},bb=W.document.removeEventListener?function(b,a,c){b.removeEventListener(a,c,!1)}:function(b,a,c){b.detachEvent("on"+a,c)};S._data=function(b){return this.cache[b[this.expando]]|| +{}};var he=/([\:\-\_]+(.))/g,ie=/^moz([A-Z])/,Hb=z("jqLite"),je=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,Ib=/<|&#?\w+;/,ke=/<([\w:]+)/,le=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,da={option:[1,'"],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};da.optgroup=da.option;da.tbody=da.tfoot=da.colgroup= +da.caption=da.thead;da.th=da.td;var Oa=S.prototype={ready:function(b){function a(){c||(c=!0,b())}var c=!1;"complete"===X.readyState?setTimeout(a):(this.on("DOMContentLoaded",a),S(W).on("load",a))},toString:function(){var b=[];r(this,function(a){b.push(""+a)});return"["+b.join(", ")+"]"},eq:function(b){return 0<=b?A(this[b]):A(this[this.length+b])},length:0,push:Pe,sort:[].sort,splice:[].splice},rb={};r("multiple selected checked disabled readOnly required open".split(" "),function(b){rb[x(b)]=b}); +var pc={};r("input select option textarea button form details".split(" "),function(b){pc[La(b)]=!0});r({data:Mb,removeData:Lb},function(b,a){S[a]=b});r({data:Mb,inheritedData:qb,scope:function(b){return A.data(b,"$scope")||qb(b.parentNode||b,["$isolateScope","$scope"])},isolateScope:function(b){return A.data(b,"$isolateScope")||A.data(b,"$isolateScopeNoTemplate")},controller:mc,injector:function(b){return qb(b,"$injector")},removeAttr:function(b,a){b.removeAttribute(a)},hasClass:Nb,css:function(b, +a,c){a=ab(a);if(D(c))b.style[a]=c;else{var d;8>=R&&(d=b.currentStyle&&b.currentStyle[a],""===d&&(d="auto"));d=d||b.style[a];8>=R&&(d=""===d?u:d);return d}},attr:function(b,a,c){var d=x(a);if(rb[d])if(D(c))c?(b[a]=!0,b.setAttribute(a,d)):(b[a]=!1,b.removeAttribute(d));else return b[a]||(b.attributes.getNamedItem(a)||v).specified?d:u;else if(D(c))b.setAttribute(a,c);else if(b.getAttribute)return b=b.getAttribute(a,2),null===b?u:b},prop:function(b,a,c){if(D(c))b[a]=c;else return b[a]},text:function(){function b(b, +d){var e=a[b.nodeType];if(F(d))return e?b[e]:"";b[e]=d}var a=[];9>R?(a[1]="innerText",a[3]="nodeValue"):a[1]=a[3]="textContent";b.$dv="";return b}(),val:function(b,a){if(F(a)){if("SELECT"===Pa(b)&&b.multiple){var c=[];r(b.options,function(a){a.selected&&c.push(a.value||a.text)});return 0===c.length?null:c}return b.value}b.value=a},html:function(b,a){if(F(a))return b.innerHTML;for(var c=0,d=b.childNodes;c":function(a,c,d,e){return d(a,c)>e(a,c)},"<=":function(a,c,d,e){return d(a,c)<=e(a,c)},">=":function(a,c,d,e){return d(a,c)>=e(a,c)},"&&":function(a,c,d,e){return d(a,c)&&e(a,c)},"||":function(a,c,d,e){return d(a,c)||e(a,c)},"&":function(a,c,d,e){return d(a, +c)&e(a,c)},"|":function(a,c,d,e){return e(a,c)(a,c,d(a,c))},"!":function(a,c,d){return!d(a,c)}},Ue={n:"\n",f:"\f",r:"\r",t:"\t",v:"\v","'":"'",'"':'"'},Ub=function(a){this.options=a};Ub.prototype={constructor:Ub,lex:function(a){this.text=a;this.index=0;this.ch=u;this.lastCh=":";for(this.tokens=[];this.index=a},isWhitespace:function(a){return" "===a||"\r"===a||"\t"===a||"\n"===a||"\v"===a||"\u00a0"===a},isIdent:function(a){return"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"_"===a||"$"===a},isExpOperator:function(a){return"-"===a||"+"===a||this.isNumber(a)}, +throwError:function(a,c,d){d=d||this.index;c=D(c)?"s "+c+"-"+this.index+" ["+this.text.substring(c,d)+"]":" "+d;throw la("lexerr",a,c,this.text);},readNumber:function(){for(var a="",c=this.index;this.index","<=",">="))a=this.binaryFn(a,c.fn,this.relational());return a},additive:function(){for(var a=this.multiplicative(),c;c=this.expect("+","-");)a=this.binaryFn(a,c.fn,this.multiplicative());return a},multiplicative:function(){for(var a=this.unary(),c;c=this.expect("*","/","%");)a=this.binaryFn(a,c.fn,this.unary());return a},unary:function(){var a;return this.expect("+")?this.primary():(a=this.expect("-"))?this.binaryFn(gb.ZERO,a.fn, +this.unary()):(a=this.expect("!"))?this.unaryFn(a.fn,this.unary()):this.primary()},fieldAccess:function(a){var c=this,d=this.expect().text,e=Dc(d,this.options,this.text);return E(function(c,d,h){return e(h||a(c,d))},{assign:function(e,g,h){(h=a(e,h))||a.assign(e,h={});return ub(h,d,g,c.text,c.options)}})},objectIndex:function(a){var c=this,d=this.expression();this.consume("]");return E(function(e,f){var g=a(e,f),h=d(e,f),k;ka(h,c.text);if(!g)return u;(g=ma(g[h],c.text))&&(g.then&&c.options.unwrapPromises)&& +(k=g,"$$v"in g||(k.$$v=u,k.then(function(a){k.$$v=a})),g=g.$$v);return g},{assign:function(e,f,g){var h=ka(d(e,g),c.text);(g=ma(a(e,g),c.text))||a.assign(e,g={});return g[h]=f}})},functionCall:function(a,c){var d=[];if(")"!==this.peekToken().text){do d.push(this.expression());while(this.expect(","))}this.consume(")");var e=this;return function(f,g){for(var h=[],k=c?c(f,g):f,m=0;ma.getHours()?c.AMPMS[0]:c.AMPMS[1]},Z:function(a){a=-1*a.getTimezoneOffset();return a=(0<=a?"+":"")+(Vb(Math[0< +a?"floor":"ceil"](a/60),2)+Vb(Math.abs(a%60),2))}},Le=/((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/,Ke=/^\-?\d+$/;Ic.$inject=["$locale"];var Ie=aa(x),Je=aa(La);Kc.$inject=["$parse"];var cd=aa({restrict:"E",compile:function(a,c){8>=R&&(c.href||c.name||c.$set("href",""),a.append(X.createComment("IE fix")));if(!c.href&&!c.xlinkHref&&!c.name)return function(a,c){var f="[object SVGAnimatedString]"===Ba.call(c.prop("href"))?"xlink:href":"href";c.on("click",function(a){c.attr(f)|| +a.preventDefault()})}}}),Fb={};r(rb,function(a,c){if("multiple"!=a){var d=qa("ng-"+c);Fb[d]=function(){return{priority:100,link:function(a,f,g){a.$watch(g[d],function(a){g.$set(c,!!a)})}}}}});r(["src","srcset","href"],function(a){var c=qa("ng-"+a);Fb[c]=function(){return{priority:99,link:function(d,e,f){var g=a,h=a;"href"===a&&"[object SVGAnimatedString]"===Ba.call(e.prop("href"))&&(h="xlinkHref",f.$attr[h]="xlink:href",g=null);f.$observe(c,function(c){c?(f.$set(h,c),R&&g&&e.prop(g,f[h])):"href"=== +a&&f.$set(h,null)})}}}});var yb={$addControl:v,$removeControl:v,$setValidity:v,$setDirty:v,$setPristine:v};Nc.$inject=["$element","$attrs","$scope","$animate"];var Qc=function(a){return["$timeout",function(c){return{name:"form",restrict:a?"EAC":"E",controller:Nc,compile:function(){return{pre:function(a,e,f,g){if(!f.action){var h=function(a){a.preventDefault?a.preventDefault():a.returnValue=!1};sb(e[0],"submit",h);e.on("$destroy",function(){c(function(){bb(e[0],"submit",h)},0,!1)})}var k=e.parent().controller("form"), +m=f.name||f.ngForm;m&&ub(a,m,g,m);if(k)e.on("$destroy",function(){k.$removeControl(g);m&&ub(a,m,u,m);E(g,yb)})}}}}}]},dd=Qc(),qd=Qc(!0),Ve=/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/,We=/^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i,Xe=/^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/,Rc={text:Ab,number:function(a,c,d,e,f,g){Ab(a,c,d,e,f,g);e.$parsers.push(function(a){var c=e.$isEmpty(a);if(c||Xe.test(a))return e.$setValidity("number", +!0),""===a?null:c?a:parseFloat(a);e.$setValidity("number",!1);return u});Ne(e,"number",Ye,null,e.$$validityState);e.$formatters.push(function(a){return e.$isEmpty(a)?"":""+a});d.min&&(a=function(a){var c=parseFloat(d.min);return ua(e,"min",e.$isEmpty(a)||a>=c,a)},e.$parsers.push(a),e.$formatters.push(a));d.max&&(a=function(a){var c=parseFloat(d.max);return ua(e,"max",e.$isEmpty(a)||a<=c,a)},e.$parsers.push(a),e.$formatters.push(a));e.$formatters.push(function(a){return ua(e,"number",e.$isEmpty(a)|| +jb(a),a)})},url:function(a,c,d,e,f,g){Ab(a,c,d,e,f,g);a=function(a){return ua(e,"url",e.$isEmpty(a)||Ve.test(a),a)};e.$formatters.push(a);e.$parsers.push(a)},email:function(a,c,d,e,f,g){Ab(a,c,d,e,f,g);a=function(a){return ua(e,"email",e.$isEmpty(a)||We.test(a),a)};e.$formatters.push(a);e.$parsers.push(a)},radio:function(a,c,d,e){F(d.name)&&c.attr("name",ib());c.on("click",function(){c[0].checked&&a.$apply(function(){e.$setViewValue(d.value)})});e.$render=function(){c[0].checked=d.value==e.$viewValue}; +d.$observe("value",e.$render)},checkbox:function(a,c,d,e){var f=d.ngTrueValue,g=d.ngFalseValue;G(f)||(f=!0);G(g)||(g=!1);c.on("click",function(){a.$apply(function(){e.$setViewValue(c[0].checked)})});e.$render=function(){c[0].checked=e.$viewValue};e.$isEmpty=function(a){return a!==f};e.$formatters.push(function(a){return a===f});e.$parsers.push(function(a){return a?f:g})},hidden:v,button:v,submit:v,reset:v,file:v},Ye=["badInput"],hc=["$browser","$sniffer",function(a,c){return{restrict:"E",require:"?ngModel", +link:function(d,e,f,g){g&&(Rc[x(f.type)]||Rc.text)(d,e,f,g,c,a)}}}],wb="ng-valid",xb="ng-invalid",Ra="ng-pristine",zb="ng-dirty",Ze=["$scope","$exceptionHandler","$attrs","$element","$parse","$animate",function(a,c,d,e,f,g){function h(a,c){c=c?"-"+nb(c,"-"):"";g.removeClass(e,(a?xb:wb)+c);g.addClass(e,(a?wb:xb)+c)}this.$modelValue=this.$viewValue=Number.NaN;this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=[];this.$pristine=!0;this.$dirty=!1;this.$valid=!0;this.$invalid=!1;this.$name= +d.name;var k=f(d.ngModel),m=k.assign;if(!m)throw z("ngModel")("nonassign",d.ngModel,ia(e));this.$render=v;this.$isEmpty=function(a){return F(a)||""===a||null===a||a!==a};var l=e.inheritedData("$formController")||yb,n=0,q=this.$error={};e.addClass(Ra);h(!0);this.$setValidity=function(a,c){q[a]!==!c&&(c?(q[a]&&n--,n||(h(!0),this.$valid=!0,this.$invalid=!1)):(h(!1),this.$invalid=!0,this.$valid=!1,n++),q[a]=!c,h(c,a),l.$setValidity(a,c,this))};this.$setPristine=function(){this.$dirty=!1;this.$pristine= +!0;g.removeClass(e,zb);g.addClass(e,Ra)};this.$setViewValue=function(d){this.$viewValue=d;this.$pristine&&(this.$dirty=!0,this.$pristine=!1,g.removeClass(e,Ra),g.addClass(e,zb),l.$setDirty());r(this.$parsers,function(a){d=a(d)});this.$modelValue!==d&&(this.$modelValue=d,m(a,d),r(this.$viewChangeListeners,function(a){try{a()}catch(d){c(d)}}))};var p=this;a.$watch(function(){var c=k(a);if(p.$modelValue!==c){var d=p.$formatters,e=d.length;for(p.$modelValue=c;e--;)c=d[e](c);p.$viewValue!==c&&(p.$viewValue= +c,p.$render())}return c})}],Fd=function(){return{require:["ngModel","^?form"],controller:Ze,link:function(a,c,d,e){var f=e[0],g=e[1]||yb;g.$addControl(f);a.$on("$destroy",function(){g.$removeControl(f)})}}},Hd=aa({require:"ngModel",link:function(a,c,d,e){e.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),ic=function(){return{require:"?ngModel",link:function(a,c,d,e){if(e){d.required=!0;var f=function(a){if(d.required&&e.$isEmpty(a))e.$setValidity("required",!1);else return e.$setValidity("required", +!0),a};e.$formatters.push(f);e.$parsers.unshift(f);d.$observe("required",function(){f(e.$viewValue)})}}}},Gd=function(){return{require:"ngModel",link:function(a,c,d,e){var f=(a=/\/(.*)\//.exec(d.ngList))&&RegExp(a[1])||d.ngList||",";e.$parsers.push(function(a){if(!F(a)){var c=[];a&&r(a.split(f),function(a){a&&c.push($(a))});return c}});e.$formatters.push(function(a){return L(a)?a.join(", "):u});e.$isEmpty=function(a){return!a||!a.length}}}},$e=/^(true|false|\d+)$/,Id=function(){return{priority:100, +compile:function(a,c){return $e.test(c.ngValue)?function(a,c,f){f.$set("value",a.$eval(f.ngValue))}:function(a,c,f){a.$watch(f.ngValue,function(a){f.$set("value",a)})}}}},id=Aa({compile:function(a){a.addClass("ng-binding");return function(a,d,e){d.data("$binding",e.ngBind);a.$watch(e.ngBind,function(a){d.text(a==u?"":a)})}}}),kd=["$interpolate",function(a){return function(c,d,e){c=a(d.attr(e.$attr.ngBindTemplate));d.addClass("ng-binding").data("$binding",c);e.$observe("ngBindTemplate",function(a){d.text(a)})}}], +jd=["$sce","$parse",function(a,c){return{compile:function(d){d.addClass("ng-binding");return function(d,f,g){f.data("$binding",g.ngBindHtml);var h=c(g.ngBindHtml);d.$watch(function(){return(h(d)||"").toString()},function(c){f.html(a.getTrustedHtml(h(d))||"")})}}}}],ld=Wb("",!0),nd=Wb("Odd",0),md=Wb("Even",1),od=Aa({compile:function(a,c){c.$set("ngCloak",u);a.removeClass("ng-cloak")}}),pd=[function(){return{scope:!0,controller:"@",priority:500}}],jc={},af={blur:!0,focus:!0};r("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "), +function(a){var c=qa("ng-"+a);jc[c]=["$parse","$rootScope",function(d,e){return{compile:function(f,g){var h=d(g[c],!0);return function(c,d){d.on(a,function(d){var f=function(){h(c,{$event:d})};af[a]&&e.$$phase?c.$evalAsync(f):c.$apply(f)})}}}}]});var sd=["$animate",function(a){return{transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,link:function(c,d,e,f,g){var h,k,m;c.$watch(e.ngIf,function(f){Wa(f)?k||(k=c.$new(),g(k,function(c){c[c.length++]=X.createComment(" end ngIf: "+e.ngIf+ +" ");h={clone:c};a.enter(c,d.parent(),d)})):(m&&(m.remove(),m=null),k&&(k.$destroy(),k=null),h&&(m=Eb(h.clone),a.leave(m,function(){m=null}),h=null))})}}}],td=["$http","$templateCache","$anchorScroll","$animate","$sce",function(a,c,d,e,f){return{restrict:"ECA",priority:400,terminal:!0,transclude:"element",controller:Xa.noop,compile:function(g,h){var k=h.ngInclude||h.src,m=h.onload||"",l=h.autoscroll;return function(g,h,p,r,J){var w=0,t,y,u,B=function(){y&&(y.remove(),y=null);t&&(t.$destroy(),t=null); +u&&(e.leave(u,function(){y=null}),y=u,u=null)};g.$watch(f.parseAsResourceUrl(k),function(f){var k=function(){!D(l)||l&&!g.$eval(l)||d()},p=++w;f?(a.get(f,{cache:c}).success(function(a){if(p===w){var c=g.$new();r.template=a;a=J(c,function(a){B();e.enter(a,null,h,k)});t=c;u=a;t.$emit("$includeContentLoaded");g.$eval(m)}}).error(function(){p===w&&B()}),g.$emit("$includeContentRequested")):(B(),r.template=null)})}}}}],Jd=["$compile",function(a){return{restrict:"ECA",priority:-400,require:"ngInclude", +link:function(c,d,e,f){d.html(f.template);a(d.contents())(c)}}}],ud=Aa({priority:450,compile:function(){return{pre:function(a,c,d){a.$eval(d.ngInit)}}}}),vd=Aa({terminal:!0,priority:1E3}),wd=["$locale","$interpolate",function(a,c){var d=/{}/g;return{restrict:"EA",link:function(e,f,g){var h=g.count,k=g.$attr.when&&f.attr(g.$attr.when),m=g.offset||0,l=e.$eval(k)||{},n={},q=c.startSymbol(),p=c.endSymbol(),s=/^when(Minus)?(.+)$/;r(g,function(a,c){s.test(c)&&(l[x(c.replace("when","").replace("Minus","-"))]= +f.attr(g.$attr[c]))});r(l,function(a,e){n[e]=c(a.replace(d,q+h+"-"+m+p))});e.$watch(function(){var c=parseFloat(e.$eval(h));if(isNaN(c))return"";c in l||(c=a.pluralCat(c-m));return n[c](e,f,!0)},function(a){f.text(a)})}}}],xd=["$parse","$animate",function(a,c){var d=z("ngRepeat");return{transclude:"element",priority:1E3,terminal:!0,$$tlb:!0,link:function(e,f,g,h,k){var m=g.ngRepeat,l=m.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/),n,q,p,s,u,w,t={$id:Na};if(!l)throw d("iexp", +m);g=l[1];h=l[2];(l=l[3])?(n=a(l),q=function(a,c,d){w&&(t[w]=a);t[u]=c;t.$index=d;return n(e,t)}):(p=function(a,c){return Na(c)},s=function(a){return a});l=g.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);if(!l)throw d("iidexp",g);u=l[3]||l[1];w=l[2];var y={};e.$watchCollection(h,function(a){var g,h,l=f[0],n,t={},D,C,I,x,G,v,z,F=[];if(Sa(a))v=a,G=q||p;else{G=q||s;v=[];for(I in a)a.hasOwnProperty(I)&&"$"!=I.charAt(0)&&v.push(I);v.sort()}D=v.length;h=F.length=v.length;for(g=0;gC;)d=u.pop(),q.removeOption(d.label),d.element.remove()}for(;B.length>Q;)B.pop()[0].element.remove()}var k;if(!(k=s.match(d)))throw bf("iexp",s,ia(f));var l=c(k[2]||k[1]), +m=k[4]||k[6],n=k[5],r=c(k[3]||""),x=c(k[2]?k[1]:m),A=c(k[7]),w=k[8]?c(k[8]):null,B=[[{element:f,label:""}]];z&&(a(z)(e),z.removeClass("ng-scope"),z.remove());f.empty();f.on("change",function(){e.$apply(function(){var a,c=A(e)||[],d={},k,l,q,r,s,t,v;if(p)for(l=[],r=0,t=B.length;r@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide{display:none !important;}ng\\:form{display:block;}.ng-animate-block-transitions{transition:0s all!important;-webkit-transition:0s all!important;}.ng-hide-add-active,.ng-hide-remove{display:block!important;}'); +//# sourceMappingURL=angular.min.js.map diff --git a/bower_components/angular/bower.json b/bower_components/angular/bower.json index e3e42892..9255bff0 100644 --- a/bower_components/angular/bower.json +++ b/bower_components/angular/bower.json @@ -1,7 +1,8 @@ { "name": "angular", - "version": "1.0.7", + "version": "1.2.27", "main": "./angular.js", + "ignore": [], "dependencies": { } } diff --git a/bower_components/handsontable/.bower.json b/bower_components/handsontable/.bower.json index b016fe86..9c416701 100644 --- a/bower_components/handsontable/.bower.json +++ b/bower_components/handsontable/.bower.json @@ -1,26 +1,35 @@ { "name": "handsontable", - "version": "0.9.19", + "description": "Spreadsheet-like data grid editor that provides copy/paste functionality compatible with Excel/Google Docs", + "version": "0.12.0", "main": [ - "./dist/jquery.handsontable.full.js", - "./dist/jquery.handsontable.full.css" + "./dist/handsontable.full.js", + "./dist/handsontable.full.css" ], "ignore": [ "**/.*", + "components", + "demo", + "lib", + "plugins", "node_modules", - "components" + "src", + "test" ], "dependencies": { - "jquery": ">=1.9.0" + "zeroclipboard": ">=2.1.6" }, - "homepage": "https://github.com/warpech/jquery-handsontable", - "_release": "0.9.19", + "devDependencies": { + "chroma-js": "~0.5.6" + }, + "homepage": "https://github.com/handsontable/jquery-handsontable", + "_release": "0.12.0", "_resolution": { "type": "version", - "tag": "v0.9.19", - "commit": "8cbf89d305fc5caa5116c0c851ca195f35953b12" + "tag": "0.12.0", + "commit": "6790df1bd5e1f5435be3ecadc2eefbcfa60274ae" }, - "_source": "git://github.com/warpech/jquery-handsontable.git", - "_target": "~0.9.11", + "_source": "git://github.com/handsontable/jquery-handsontable.git", + "_target": "~0.12", "_originalSource": "handsontable" } \ No newline at end of file diff --git a/bower_components/handsontable/CHANGELOG.md b/bower_components/handsontable/CHANGELOG.md index cd5f1c66..dd28c1fe 100644 --- a/bower_components/handsontable/CHANGELOG.md +++ b/bower_components/handsontable/CHANGELOG.md @@ -1,811 +1 @@ -## [0.9.19](https://github.com/warpech/jquery-handsontable/tree/v0.9.19) (Oct 01, 2013) - -Two features that come in handy for plugin developers: -- new plugin hook: `afterRenderer` -- (previously private) DOM helpers are now exposed as `Handosontable.Dom` (see [api](https://github.com/warpech/jquery-handsontable/blob/master/src/3rdparty/walkontable/src/dom.js)) - -## [0.9.18](https://github.com/warpech/jquery-handsontable/tree/v0.9.18) (Sep 19, 2013) - -Features: -- native browser scrollbars feature becomes usable, but currently only works vertically ([demo](http://handsontable.com/demo/scroll_native.html)) - -Bugfixes: -- it was possible to move a column by just double clicking on move handle ([#963](https://github.com/warpech/jquery-handsontable/issues/963)) -- can't edit a cell that is outside of the viewport ([#1035](https://github.com/warpech/jquery-handsontable/issues/1035)) -- context-menu-layer was not removed from DOM after $.contextMenu destroy -- cleanup CSS from excessive vendor prefixes -- performance improvement of scrolling with autoColumnSize - -## [0.9.17](https://github.com/warpech/jquery-handsontable/tree/v0.9.17) (Sep 5, 2013) - -Features: -- `beforeRemoveRow` and `beforeRemoveCol` events are now invoked with absolute index -- if table has custom column headers, removing column will also remove the corresponding header - -Bugfix: - -- fixed crashing the whole page when autocomplete is on ([#1011](https://github.com/warpech/jquery-handsontable/issues/1011)) -- fixed changing multiple cells values when using autocomplete ([#1021](https://github.com/warpech/jquery-handsontable/issues/1021)) -- fixed handling multiple tables with different sorting options on the same page ([#1020](https://github.com/warpech/jquery-handsontable/issues/1020)) -- fixed undoing row removal from tables which data source is array of objects ([#966](https://github.com/warpech/jquery-handsontable/issues/966)) -- fixed undoing column removal -- fixed removing columns form table which has more rows than can be rendered in viewport ([#1012](https://github.com/warpech/jquery-handsontable/issues/1012)) -- fixed marking Undo/Redo in context menu as enabled/disabled -- fixed adding new rows directly to data source, when table is sorted ([#858](https://github.com/warpech/jquery-handsontable/issues/858)) - -## [0.9.16](https://github.com/warpech/jquery-handsontable/tree/v0.9.16) (Aug 27, 2013) - -Features: - -- New cell type: `password` -- Rebuilt UndoRedo module - -Bugfix: - -- fixed using manualColumnMove with multiple HOT instances ([#999](https://github.com/warpech/jquery-handsontable/issues/999)) -- fixed autoWrapCol and autoWrapRow behaviour ([#992](https://github.com/warpech/jquery-handsontable/issues/992)) -- autocomplete fields will now behave the same as regular fields after closing editor by clicking on another cell (if in non strict mode) ([#991](https://github.com/warpech/jquery-handsontable/issues/991)) -- fixed validation after changing column order ([#980](https://github.com/warpech/jquery-handsontable/issues/980)) - -## [0.9.15](https://github.com/warpech/jquery-handsontable/tree/v0.9.15) (Aug 26, 2013) - -Features: -- `colWidths` property of the constructor may now be of type: number, string, array, function (was only array) ([#947](https://github.com/warpech/jquery-handsontable/issues/947), [#997](https://github.com/warpech/jquery-handsontable/issues/997)) -- `widths` property of `columns` or `cells` option may now be of type: number, string (was only number) - -Bugfix: -- autoColumnSize now calculates width using the actual cell renderer ([#486](https://github.com/warpech/jquery-handsontable/issues/486)) - -## [0.9.14](https://github.com/warpech/jquery-handsontable/tree/v0.9.14) (Aug 20, 2013) - -Bugfixes: - -- fixed selecting date using jQuery UI Datepicker ([#970](https://github.com/warpech/jquery-handsontable/issues/970)) -- fixed opening cell editor after clearing cell data with Delete or Backspace ([#975](https://github.com/warpech/jquery-handsontable/issues/975)) - -## [0.9.13](https://github.com/warpech/jquery-handsontable/tree/v0.9.13) (Aug 16, 2013) - -Features: -- removed the need to declare `.handsontable .htCore td` in user CSS. Now `.handsontable td` works ok ([#965](https://github.com/warpech/jquery-handsontable/issues/965), [#956](https://github.com/warpech/jquery-handsontable/issues/956)) - -Bugfixes: -- cellProperties were not refreshed after removing a row ([#959](https://github.com/warpech/jquery-handsontable/issues/959)) -- manual column resize handles were misplaced when table changes width ([#949](https://github.com/warpech/jquery-handsontable/issues/949)) -- when no cells have focus CTRL+V pastes into all cells up to last cell to have focus ([#946](https://github.com/warpech/jquery-handsontable/issues/946)) -- afterColumnResize callback can be called with an incorrect size, or undefined ([#945](https://github.com/warpech/jquery-handsontable/issues/945)) -- can't start cell editing while holding SHIFT ([#944](https://github.com/warpech/jquery-handsontable/issues/944)) -- autocomplete text editor was throwing afterChange event twice ([#939](https://github.com/warpech/jquery-handsontable/issues/939)) -- fixed inserting and removing rows when columnSorting is enabled, but table hasn't been sorted yet ([#915](https://github.com/warpech/jquery-handsontable/issues/915)) -- toggling checkbox state using spacebar did not update the data source and trigger afterChange event ([#895](https://github.com/warpech/jquery-handsontable/issues/895)) -- context menu functions did not work properly when the cell selection was performed upwards or leftwards ([#674](https://github.com/warpech/jquery-handsontable/issues/674)) -- `observeChanges` should register only external changes, not changes made with Handsontable (which led to plugin hooks triggered twice) -- ~~error adding row when column sorting is in effect ([#858](https://github.com/warpech/jquery-handsontable/issues/858))~~ - still not fixed. We will address it with high priority -- Opening autocomplete list, hovering over a list item and then clicking outside of the table will close the editor and won't change cell value ([#638](https://github.com/warpech/jquery-handsontable/issues/638)) - -## [0.9.12](https://github.com/warpech/jquery-handsontable/tree/v0.9.12) (Aug 7, 2013) - -Features: - -- closing cell editors when table is being scrolled ([#914](https://github.com/warpech/jquery-handsontable/issues/914)) -- added `sort()` method to programmatically sort table -- plugin `contextMenu` can now be enabled or disabled using `updateSettings()` method -- extension `removeRow` can now be enabled or disabled using `updateSettings()` method ([#934](https://github.com/warpech/jquery-handsontable/issues/934)) -- plugin `observeChanges` can now be enabled or disabled using `updateSettings()` method -- plugin `autoColumnSize` can now be enabled or disabled using `updateSettings()` method -- 2 new events: `beforColumnSort` and `afterColumnSort` fired before and after table sort - -Bugfixes: - -- fixed incorrect width of horizontal scrollbar when scrolled to rightmost column ([#909](https://github.com/warpech/jquery-handsontable/issues/909)) -- fix ability to check/uncheck checkboxes using spacebar ([#895](https://github.com/warpech/jquery-handsontable/issues/895)) -- added more specific selectors in CSS, so that jQuery UI styles and default HandsonTable styles does not interfere ([#498](https://github.com/warpech/jquery-handsontable/issues/498)) -- fixed moving table column, when table is scrolled horizontally ([#527](https://github.com/warpech/jquery-handsontable/issues/527)) -- `afterRender` event is now fired after every table scroll ([#733](https://github.com/warpech/jquery-handsontable/issues/733)) -- fixed inserting and removing rows form sorted table ([#915](https://github.com/warpech/jquery-handsontable/issues/915)) -- added proper mapping of cell properties when table is sorted ([#917](https://github.com/warpech/jquery-handsontable/issues/917)) -- fixed IE `Array.filter()` shim ([#934](https://github.com/warpech/jquery-handsontable/issues/934)) -- fixed tests, so that they all pass on Sauce Labs servers -- fixed support for legacy syntax in cells method `return {type: {renderer: function(){ /*...*/ }}` -- autoColumnSize plugin no longer tests column width using number '9999999999' (now it takes first value from data source) - -## [0.9.11](https://github.com/warpech/jquery-handsontable/tree/v0.9.11) (Jul 29, 2013) - -This version fixes some severe cell listener issues introduced in the last version. - -Bugfixes: -- selecting a cell in another table when already focused on an existing table does not bring focus to the new table ([#924](https://github.com/warpech/jquery-handsontable/issues/924)) -- cannot change more than 2 numeric cells ([#911](https://github.com/warpech/jquery-handsontable/issues/911), [#922](https://github.com/warpech/jquery-handsontable/issues/922)) -- Focus lost after Ctrl+(A or Z or Y) ([#910](https://github.com/warpech/jquery-handsontable/issues/910), [#893](https://github.com/warpech/jquery-handsontable/issues/893)) - -## [0.9.10](https://github.com/warpech/jquery-handsontable/tree/v0.9.10) (Jul 23, 2013) - -Features: -- New option: `persistentState` ([docs](https://github.com/warpech/jquery-handsontable/wiki/Options)). Settings of manualColumnMove, manualColumnResize and columnSorting can be restored on page load using localStorage. See [Column resize](http://handsontable.com/demo/column_resize.html) and [Sorting](http://handsontable.com/demo/sorting.html) demos - -Bugfixes (sorting): -- issues with create/update/delete operations on a sorted table ([#793](https://github.com/warpech/jquery-handsontable/issues/793), [#542](https://github.com/warpech/jquery-handsontable/issues/542), [#746](https://github.com/warpech/jquery-handsontable/issues/746)) -- added proper sorting for `date` column type ([#720](https://github.com/warpech/jquery-handsontable/issues/720)) -- sorting tables with spare rows that contain not empty cells ([#857](https://github.com/warpech/jquery-handsontable/issues/857)) - -Bugfixes (validation): -- cell editors now work better with async cell validators -- validator should add class name `htInvalid` to a cell without removing other classes - -Bugfixes (big cells): -- remove maximum column width 200px limit ([#422](https://github.com/warpech/jquery-handsontable/issues/422)) -- refactor manualColumnResize plugin, now it works ok also with columns wider than viewport -- refactor scrollViewport to correctly scroll to cells wider than viewport (should just cut the part that is not visible) -- refactor scrollViewport to correctly scroll to cells higher than viewport (should just cut the part that is not visible) -- cell editor could not handle long values correctly ([#393](https://github.com/warpech/jquery-handsontable/issues/393), [#441](https://github.com/warpech/jquery-handsontable/issues/441), [#804](https://github.com/warpech/jquery-handsontable/issues/804)) -- scrollbar handle size does not depend on row height anymore, which solves problem of jumpy scrollbar behavior for rows of variable height [SC#527](https://github.com/Starcounter/Starcounter/issues/527) -- it should not be allowed to select fragments of Handsontable chrome (even if `fragmentSelection` set to true) -- fix cell selection positioning for tables with `` - -Bugfixes (other): -- removing rows from table with fixed rows ([#805](https://github.com/warpech/jquery-handsontable/issues/805)) -- issues with selecting inputs/textareas/selects outside of HOT ([#408](https://github.com/warpech/jquery-handsontable/issues/408)) -- fixed API methods `addHook`, `addHookOnce`, `removeHook`, `runHooks`, `runHooksAndReturn` to manipulate local hooks -- fixed calling `beforeChange` and `afterChange` hooks multiple times while editing a single value in a column with synchronous validator ([#864](https://github.com/warpech/jquery-handsontable/issues/864)) - -Performance: -- replace jQuery `find` with `querySelector` where possible -- replace jQuery `index` with `wtDom.index` where possible - up to 8x faster -- replace jQuery `:visible` with `wtDom.isVisible` where possible - -Other: -- updated Grunt packages -- added livereload to `grunt watch` - works with Chrome LiveReload extension. See: https://github.com/gruntjs/grunt-contrib-watch -- remove the deprecated `isWritable` cell property from code and demo; add it temporarily to `src/plugins/legacy.js` so it does not break your code (to be removed in 1.0 or sooner) -- now all tests pass in IE8-10, FF, Ch, also with `grunt sauce` - -## [0.9.9](https://github.com/warpech/jquery-handsontable/tree/v0.9.9) (Jun 30, 2013) - -Bugfix: -- version 0.9.8 contained a fatal typo in cut (CTRL+X) callback - -## [0.9.8](https://github.com/warpech/jquery-handsontable/tree/v0.9.8) (Jun 30, 2013) - -Bugfix: -- copy/cut/paste did not work since last version ([#846](https://github.com/warpech/jquery-handsontable/issues/846)) - -## [0.9.7](https://github.com/warpech/jquery-handsontable/tree/v0.9.7) (Jun 30, 2013) - -Features: -- new option `fragmentSelection`, which unblocks free text selection within cells ([demo](http://handsontable.com/demo/options.html), [docs](https://github.com/warpech/jquery-handsontable/wiki/Options)) - -Bugfixes: -- `updateSettings` method did not take effect when updating `readOnly`, `columns`, `colHeaders`, `fixedColumnsLeft`, `fixedRowsTop` ([#824](https://github.com/warpech/jquery-handsontable/issues/824), [#715](https://github.com/warpech/jquery-handsontable/issues/715), [#524](https://github.com/warpech/jquery-handsontable/issues/524)) -- numeric cell type did not respect the `readOnly` setting -- `demo/php.html` is now fixed (runs only when PHP is installed, does not work on handsontable.com) -- mousewheel not working in IE8 ([#817](https://github.com/warpech/jquery-handsontable/issues/817)) -- HTML in cells and headers was escaped when it shouldn't be ([#845](https://github.com/warpech/jquery-handsontable/issues/845), [#815](https://github.com/warpech/jquery-handsontable/issues/815), [#821](https://github.com/warpech/jquery-handsontable/issues/821)) -- fixed issues with asynchronous validators - -Other: -- all Handsontable event listeners now listen on document body. This was needed to make `fragmentSelection` feature -- created the [Handsontable Google Group](https://groups.google.com/forum/#!forum/handsontable). Please use it for general questions - and keep GitHub Issues for bugfixes and feature requests - -Tests: -- initial integration with Sauce Labs (`grunt sauce`). More info about testing coming soon in wiki -- dropped support for IE7, which now has [below 1%](http://gs.statcounter.com/#browser_version_partially_combined-ww-monthly-201306-201306-bar) of the global market share. IE8 is safe for now - -## [0.9.6](https://github.com/warpech/jquery-handsontable/tree/v0.9.6) (Jun 18, 2013) - -Bugfixes: -- `isEmptyRow` produced error `this.countCols is not a function` ([#632](https://github.com/warpech/jquery-handsontable/issues/632)) -- delete row extension does not show the button when grid is inside a `` ([#764](https://github.com/warpech/jquery-handsontable/issues/764)) -- drag-down not working if Handsontable is inside a table ([#355](https://github.com/warpech/jquery-handsontable/issues/355), [#361](https://github.com/warpech/jquery-handsontable/issues/361), [#538](https://github.com/warpech/jquery-handsontable/issues/538), [#438](https://github.com/warpech/jquery-handsontable/issues/438), [#671](https://github.com/warpech/jquery-handsontable/issues/671), [#704](https://github.com/warpech/jquery-handsontable/issues/704)) - this makes me realize how many people still use tables to create a layout -- numeric cell renderer did not add class name `htDimmed` to a read only cell - -Performance: -- avoid jQuery and expensive DOM operations in several places - -Other: -- add more test cases -- upgrade jQuery contextMenu plugin to v1.6.5 -- upgrade jquery-mousewheel plugin to v3.1.3 - -## [0.9.5](https://github.com/warpech/jquery-handsontable/tree/v0.9.5) (Jun 15, 2013) - -Feature: -- cell validators! See the redone [Validation](http://handsontable.com/demo/validation.html) demo page. New plugin cell options: `validator`, `allowInvalid`. New plugin hooks: `beforeValidate`, `afterValidate` - -Bugfixes: -- read only cells: `htDimmed` overrides all classes ([#699](https://github.com/warpech/jquery-handsontable/issues/699)) -- calling 'loadData' after a change made by a paste event will produce an error in the hooks invocation ([#783](https://github.com/warpech/jquery-handsontable/issues/783)) -- autoColumnSize did not take `
` class into account when measuring the column width -- column header width was not applied correctly where there were no rows in the data source -- methods `countVisibleRows` and `countVisibleCols` threw an exception if table is not rendered (now they return -1) - -Performance: -- use `cloneNode` in cell built-in cell renderers for better performance -- fix double rendering in populateFromArray method -- cellProperties are now cached (not created on the fly) - -Other: -- add `grunt-contrib-connect` to `Gruntfile.js` that allows to run an ad-hoc http://localhost:8080/ server with command `grunt connect` -- ensure good integration with [Bower](http://bower.io/) front-end package manager -- bind `cellProperties` as `this` in `cells` callback - -## [0.9.4](https://github.com/warpech/jquery-handsontable/tree/v0.9.4) (Jun 7, 2013) - -Bugfixes: -- should scroll to last row with very high rows respecting fixedRows ([#758](https://github.com/warpech/jquery-handsontable/issues/758)) -- Native Scrollbar was listening to window scroll also after table was removed from DOM -- `numeric` cell type now correctly formats negative values and strings with dot character ([#658](https://github.com/warpech/jquery-handsontable/issues/658)) -- fix for returning false on `beforeChange` callback has no effect ([#749](https://github.com/warpech/jquery-handsontable/issues/749)) -- clicking on partially visible cell scrolled the table but did not select the desired cell - -Other: -- [Numeric](http://handsontable.com/demo/numeric.html) cell type demo now shows default (USD) and custom (EUR) locale for currency formatting - -## [0.9.3](https://github.com/warpech/jquery-handsontable/tree/v0.9.3) (Jun 4, 2013) - -Bugfixes: -- scrolling to the last row/column did not work with rows of big height, or columns of big width ([#645](https://github.com/warpech/jquery-handsontable/issues/645), [#698](https://github.com/warpech/jquery-handsontable/issues/698), [#730](https://github.com/warpech/jquery-handsontable/issues/730)) -- fix `observeChanges` in IE9-10 and Firefox (merge Object.observe shim fixes from https://github.com/Starcounter-Jack/JSON-Patch/pull/6) -- initial render was incomplete with Native Scrollbars on -- flat notation of options `cellProperties.renderer`, `cellProperties.editor` did not work as desired ([#713](https://github.com/warpech/jquery-handsontable/issues/713)) -- cut and paste events did not work in a reliable way in Mac Chrome and Safari - -Other: -- make [Callbacks](http://handsontable.com/demo/callbacks.html) demo faster and more convenient -- change async tests to sync where possible - -## [0.9.2](https://github.com/warpech/jquery-handsontable/tree/v0.9.2) (May 28, 2013) - -Features: -- initial release of Native Scrollbars feature (experimental, don't use yet) -- initial release of `observeChanges` option (experimental). Available for all supported browsers except IE 7-8 (uses Object.observe when possible or a custom shim based on [Starcounter-Jack/JSON-Patch](https://github.com/Starcounter-Jack/JSON-Patch)) - -Bugfixes: -- mouse wheel didn't scroll the window when cursor was over Handsontable ([#383](https://github.com/warpech/jquery-handsontable/issues/383), [#627](https://github.com/warpech/jquery-handsontable/issues/627)) -- methods `countVisibleRows` and `countVisibleCols` were broken since version 0.9.0 ([#711](https://github.com/warpech/jquery-handsontable/issues/711)) -- WalkontableDom.prototype.offset now returns offset relatively to the document also for position: fixed -- fix scrolling on border cases when table height was the size of the container - -Docs: -- new demo page [Options](http://handsontable.com/demo/options.html). Currently features one option but this will improve over time -- added new file [CONTRIBUTING.md](CONTRIBUTING.md). Will be updated with more information soon - -## [0.9.1](https://github.com/warpech/jquery-handsontable/tree/v0.9.1) (May 22, 2013) - -Bugfix: -- `beforeKeyDown` event is now also triggered from text editor handler - -Docs: -- demo page [Callbacks](http://handsontable.com/demo/callbacks.html) now features all callbacks -- new demo page for [beforeKeyDown](http://handsontable.com/demo/beforeKeyDown.html) event - -## [0.9.0](https://github.com/warpech/jquery-handsontable/tree/v0.9.0) (May 21, 2013) - -Features: -- cell renderer and editor may now be declared as strings provided they are aliased in the lookup map ([docs](https://github.com/warpech/jquery-handsontable/wiki/Options#column-options), [demo](http://handsontable.com/demo/conditional.html), [#667](https://github.com/warpech/jquery-handsontable/issues/667)) -- new event hook `beforeKeyDown` ([docs](https://github.com/warpech/jquery-handsontable/wiki/Events)) - -Bugfixes: -- fix demo page buttons.html ([#602](https://github.com/warpech/jquery-handsontable/issues/602)) - -Web Component updates: -- update [Web Component demo](http://handsontable.com/) with tiny dashboard -- update Toolkitchen Toolkit to Polymer (@3aec92c) -- use unmodified Handsontable JS and CSS files inside the component -- include all Web Component dependencies inside HTML Import (you just import Polymer and one HTML file!) -- Web Component now uses jQuery 2.0.0 (bundled in), but in future will not depend on jQuery -- remove jQuery UI datepicker from Web Component version (it won't work, as described here: http://bugs.jquery.com/ticket/13342) - -Other: -- date cell type: upgrade to jQuery UI 1.10.3 -- introduce WalkontableViewport abstraction -- fix problem with row header fixed width (now width is dynamically checked) ([#402](https://github.com/warpech/jquery-handsontable/issues/402), [#475](https://github.com/warpech/jquery-handsontable/issues/475)) - -## [0.9.0-beta2](https://github.com/warpech/jquery-handsontable/tree/v0.9.0-beta2) (May 15, 2013) - -Features: -- consistency: now all `on*` events are renamed to `after*` (see [Events](https://github.com/warpech/jquery-handsontable/wiki/Events) wiki page) -- new paste methods ([docs](https://github.com/warpech/jquery-handsontable/wiki/Options)) -- new methods: - - getDataAtRow ([docs](https://github.com/warpech/jquery-handsontable/wiki/Methods)) - - getDataAtCol ([docs](https://github.com/warpech/jquery-handsontable/wiki/Methods)) - - getDataAtProp ([docs](https://github.com/warpech/jquery-handsontable/wiki/Methods)) - - spliceCol ([docs](https://github.com/warpech/jquery-handsontable/wiki/Methods)) - - spliceRow ([docs](https://github.com/warpech/jquery-handsontable/wiki/Methods)) - -Bugfixes: -- fix problem with clearing `animate` function interval ([#456](https://github.com/warpech/jquery-handsontable/issues/456)) -- fix auto resize column when double clicked on `.manualColumnResizer` ([#594](https://github.com/warpech/jquery-handsontable/issues/594)) -- fix undo/redo event handler (change `datachange.handsontable` event to `afterChange` hook) -- fix inline styles in "Edit in JSFiddle" code generation -- fix horizontal scrolling in IE8 ([#583](https://github.com/warpech/jquery-handsontable/issues/583)) - -Other: - -- new demo pages: [Pagination](http://handsontable.com/demo/pagination.html) and [Search](http://handsontable.com/demo/search.html) - -## [0.9.0-beta1](https://github.com/warpech/jquery-handsontable/tree/v0.9.0-beta1) (May 7, 2013) - -Features: -- improved [Options](https://github.com/warpech/jquery-handsontable/wiki/Options) model. Cell properties now inherit from Column properties constructor, which inherit from Handsontable Constructor. This is based on JavaScript prototypal inheritance, rather than `$.extend` as previously -- new [Events](https://github.com/warpech/jquery-handsontable/wiki/Events) architecture (common for callbacks and plugin hooks) -- new events available: - - afterRender ([docs](https://github.com/warpech/jquery-handsontable/wiki/Events), [#597](https://github.com/warpech/jquery-handsontable/issues/597)) - - afterColumnResize ([docs](https://github.com/warpech/jquery-handsontable/wiki/Events), [#557](https://github.com/warpech/jquery-handsontable/issues/557)) - - afterColumnMove ([docs](https://github.com/warpech/jquery-handsontable/wiki/Events), [#557](https://github.com/warpech/jquery-handsontable/issues/557)) - - afterCreateRow ([docs](https://github.com/warpech/jquery-handsontable/wiki/Events), [#122](https://github.com/warpech/jquery-handsontable/issues/122), [#344](https://github.com/warpech/jquery-handsontable/issues/344)) - - afterCreateCol ([docs](https://github.com/warpech/jquery-handsontable/wiki/Events), [#122](https://github.com/warpech/jquery-handsontable/issues/122)) - - afterRemoveRow ([docs](https://github.com/warpech/jquery-handsontable/wiki/Events), [#69](https://github.com/warpech/jquery-handsontable/issues/69), [#227](https://github.com/warpech/jquery-handsontable/issues/227)) - - afterRemoveCol ([docs](https://github.com/warpech/jquery-handsontable/wiki/Events)) - - beforeAutofill ([docs](https://github.com/warpech/jquery-handsontable/wiki/Events), [#200](https://github.com/warpech/jquery-handsontable/issues/200), [#133](https://github.com/warpech/jquery-handsontable/issues/133), [#36](https://github.com/warpech/jquery-handsontable/issues/36)) -- `setDataAtCell` method now accepts `source` string as second parameter (if first parameter is `changes` array) - -Bugfixes: -- fix problem of rendering an unspecified height table with no rows -- `onBeforeChange` now allows to remove one change from array by assigning it null value (`changes[i] = null`) - -Other: -- move `jquery.handsontable.js` and `jquery.handsontable.css` to `dist/` - -## [0.8.23](https://github.com/warpech/jquery-handsontable/tree/v0.8.23) (May 3, 2013) - -Features: -- Handsontable will not be rendered if the container is not attached to DOM or it's style is `display: none` -- new configuration option `observeDOMVisibility` (default `true`) will attempt to rerender it once it is attached to DOM or style changed to `display: block` - -Bugfixes: -- Backbone Collections throw error in loadData ([#606](https://github.com/warpech/jquery-handsontable/issues/606)) -- auto column size did not consider CSS style of each instance separately -- vertical mousewheel scrolling does not work without horizontal scrollbar ([#541](https://github.com/warpech/jquery-handsontable/issues/541)) - -## [0.8.22](https://github.com/warpech/jquery-handsontable/tree/v0.8.22) (Apr 29, 2013) - -Features: -- first version to support [W3C Web Components](http://handsontable.com/demo/web_component.html)! In this proof-of-concept state, better don't bother to follow that link in browser different than Chrome -- text renderer now adds class name `htDimmed` to read only cells -- `TD.htDimmed` rule added to CSS file - -Bugfix: -- table with set `width` and undefined `height` was shrinking on window resize -- onChange fired twice when changing an autocomplete cell (fixes [#260](https://github.com/warpech/jquery-handsontable/issues/260), [#530](https://github.com/warpech/jquery-handsontable/issues/530), [#531](https://github.com/warpech/jquery-handsontable/issues/531), [#534](https://github.com/warpech/jquery-handsontable/issues/534)) - -## [0.8.21](https://github.com/warpech/jquery-handsontable/tree/v0.8.21) (Apr 24, 2013) - -Feature: -- [Fixed rows & columns](http://handsontable.com/demo/fixed.html) - -Bugfix: -- focusCatcher produced a 1x1 px red pixel in top left corner - -## [0.8.20](https://github.com/warpech/jquery-handsontable/tree/v0.8.20) (Apr 19, 2013) - -Bugfixes: -- source parameter should be `edit` when cell value is changed through editor ([#566](https://github.com/warpech/jquery-handsontable/issues/566)) -- getRowHeaders returns NaN when no argument given - should return array of all row headers ([#568](https://github.com/warpech/jquery-handsontable/issues/568), [#352](https://github.com/warpech/jquery-handsontable/issues/352)) -- getColHeaders returns NaN when no argument given - should return array of all column headers ([#569](https://github.com/warpech/jquery-handsontable/issues/569), [#382](https://github.com/warpech/jquery-handsontable/issues/382)) -- selected area class name was not applied on scrolling (tdCache was not bound to instance) -- fix cell focusing problems ([#549](https://github.com/warpech/jquery-handsontable/issues/549), [#506](https://github.com/warpech/jquery-handsontable/issues/506), [#573](https://github.com/warpech/jquery-handsontable/issues/573)) -- currentRowClassName and currentColClassName not kept while scrolling ([#455](https://github.com/warpech/jquery-handsontable/issues/455)) - -Other: -- refactor row and column header DOM operations to be consistently defined in `tableView.js` -- remove `asyncRendering` mode that was causing more trouble than benefit -- as the result, code is now 100 lines shorter and more stable! - -## [0.8.19](https://github.com/warpech/jquery-handsontable/tree/v0.8.19) (Apr 12, 2013) - -Bugfix: -- table width was not correctly read from container width - -## [0.8.18](https://github.com/warpech/jquery-handsontable/tree/v0.8.18) (Apr 12, 2013) - -Features: -- added "Maximize HOT table" button in first example on [Scroll demo](handsontable.com/demo/scroll.html) page ([#495](https://github.com/warpech/jquery-handsontable/issues/495)) - -Bugfixes: -- clicking on checkbox renderer does not change state ([#543](https://github.com/warpech/jquery-handsontable/issues/543)) -- checkbox readonly is not working ([#555](https://github.com/warpech/jquery-handsontable/issues/555)) - -Other: -- refactored Walkontable code that measures column width strategy (for better performance and stability) -- moved reading DOM settings from tableView.js core.js (new method `parseSettingsFromDOM`) - -## [0.8.17](https://github.com/warpech/jquery-handsontable/tree/v0.8.17) (Mar 31, 2013) - -Features: -- performance: remove jQuery dependency from many places -- performance: in cell renderers, replace `innerHTML` with `createTextNode` -- better integration with Twitter Bootstrap (fixes [#78](https://github.com/warpech/jquery-handsontable/issues/78)) - -Bugfixes: -- empty cell not shown in Firefox when not in standards mode ([#418](https://github.com/warpech/jquery-handsontable/issues/418)). This is questionable as a bug but anyway we fixed it. -- continued fix for #461 -- fix problems when removing rows/columns using context menu ([#523](https://github.com/warpech/jquery-handsontable/pull/523)) - -Other: -- new editor inheritance model ([#516](https://github.com/warpech/jquery-handsontable/pull/516)) -- `src/3rdparty/walkontable.js` has been divided into many source files in `src/3rdparty/walkontable/src/*`. Build process is updated to use Walkontable source files now -- 3 Grunt test tasks are available now: `grunt test`, `grunt test:handsontable`, `grunt test:walkontable` - -## [0.8.16](https://github.com/warpech/jquery-handsontable/tree/v0.8.16) (Mar 26, 2013) - -Features: -- [Handsontable in Handsontable editor](http://handsontable.com/demo/handsontable.html) -- performance and code quality fixes -- `height` and `width` settings can now be functions (that return a number) - -Bugfixes: -- autocomplete menu did not reset `
  • ` margin -- `destroy()` of one Handsontable instance made other instances on the same page unfunctional -- Autocomplete (Bootstrap Typeahead) did not allow empty string as a value ([#254](https://github.com/warpech/jquery-handsontable/issues/254)) -- outsideClickDeselects : false disables all focus in other inputs outside the table ([#408](https://github.com/warpech/jquery-handsontable/issues/408)) -- can't scroll all the way horizontally ([#430](https://github.com/warpech/jquery-handsontable/issues/430)) -- scrollable Handsontable does not work well with Twitter Bootstrap ([#224](https://github.com/warpech/jquery-handsontable/issues/224)) -- weirdly shrinking table when height attribute was not set ([#461](https://github.com/warpech/jquery-handsontable/issues/461)) - -Other: -- trying to load a string as a data source now throws an error - -## [0.8.15](https://github.com/warpech/jquery-handsontable/tree/v0.8.15) (Mar 18, 2013) - -Features: -- new callbacks: `onSelectionEnd` and `onSelectionEndByProp` (see [README.md](https://github.com/warpech/jquery-handsontable) for usage information) -- new demo page [Callbacks](http://handsontable.com/demo/callbacks.html) -- **breaking change:** `getSelected` method output has slightly changed. Was: [`topLeftRow`, `topLeftCol`, `bottomRightRow`, `bottomRightCol`]. Is: [`startRow`, `startCol`, `endRow`, `endCol`]. This gives information about the direction of the selection - now you can tell if the selection started from left to right or otherwise. - -Bugfixes: -- `outsideClickDeselects: false` disables all focus in other inputs outside the table ([#408](https://github.com/warpech/jquery-handsontable/issues/408)) -- copy paste in Mac Safari didn't work ([#470](https://github.com/warpech/jquery-handsontable/issues/470)) -- table jumps back and forth when scrolling ([#432](https://github.com/warpech/jquery-handsontable/issues/432)) -- destroy doesn't unload context menu functionality ([#434](https://github.com/warpech/jquery-handsontable/issues/434)) -- throwed error when Enter key was pressed on autocomplete cell in the last row ([#497](https://github.com/warpech/jquery-handsontable/issues/497)) -- Autocomplete in strict mode allows values other than predefined ([#405](https://github.com/warpech/jquery-handsontable/issues/405)) - -## [0.8.14](https://github.com/warpech/jquery-handsontable/tree/v0.8.14) (Mar 14, 2013) - -Features: -- now, your data source can be a function! See the last section of the [Array, object and function data sources](http://handsontable.com/demo/datasources.html) page for examples (fixes [#471](https://github.com/warpech/jquery-handsontable/pull/471), [#435](https://github.com/warpech/jquery-handsontable/pull/435), [#261](https://github.com/warpech/jquery-handsontable/issues/261)) -- also the column `data` property can be a function as well! Again, see the last section of the [data sources](http://handsontable.com/demo/datasources.html) page for examples -- new methods: `countEmptyRows`, `countEmptyCols`, `isEmptyRow`, `isEmptyCol`. You can define your own functions for `isEmptyRow` and `isEmptyCol` (see [README.md](https://github.com/warpech/jquery-handsontable) for details) - -Bugfix: -- replace reference to non-existent Exception with Error ([#494](https://github.com/warpech/jquery-handsontable/pull/494)) -- textarea copyPaste covering page elements ([#488](https://github.com/warpech/jquery-handsontable/issues/488), [#490](https://github.com/warpech/jquery-handsontable/issues/490)) - -## [0.8.13](https://github.com/warpech/jquery-handsontable/tree/v0.8.13) (Mar 12, 2013) - -This release adds a `min-width: 600px` to the test suite to make sure that 100% of current tests are passing in [Travis CI](https://travis-ci.org/warpech/jquery-handsontable) (PhantomJS) as well as in the desktop browsers. - -There are still many bugs to be fixed, but now it should be much easier to keep the increasing quality of future versions. - -## [0.8.12](https://github.com/warpech/jquery-handsontable/tree/v0.8.12) (Mar 12, 2013) - -This release doesn't really bring any improvements for the end user but is a big step towards test automation. From now on, a push to the `master` branch triggers a [Travis CI](https://travis-ci.org/warpech/jquery-handsontable) build that performs automated testing using [grunt-contrib-jasmine](https://github.com/gruntjs/grunt-contrib-jasmine). Thanks @bollwyvl for your [help](https://github.com/warpech/jquery-handsontable/pull/474)! - -As a side effect, tests are now be runnable using PhantomJS. To run the test suite in PhantomJS, type `grunt test` (first run `npm install` to make sure you have all dependencies installed). PhantomJS runs the test suite much faster than desktop browsers, so it may come handy. - -Bugfixes: -- `destroy` method now clears all pending timeouts - -## [0.8.11](https://github.com/warpech/jquery-handsontable/tree/v0.8.11) (Mar 8, 2013) - -Features: -- this may be a **breaking change** for some applications: Now the third parameter to the `alter` method tells the amount of rows/columns to be inserted/removed. This adds more consistency to the API. ([#368](https://github.com/warpech/jquery-handsontable/issues/368), [67fe67c](https://github.com/warpech/jquery-handsontable/commit/67fe67c3cf6bf4df47c02ea8c7aa6802864e97de)) -- merged pull request [#474](https://github.com/warpech/jquery-handsontable/pull/474) - Travis-CI integration. The tests don't pass yet but don't worry about it yet. It is a work in progress on complete test automation. Making all tests pass on Travis CI headless browser may take few more days or weeks :) - -Bugfix: -- again, this may be a **breaking change** for some applications: Fix very long standing inconsistency. Restored original `setDataAtCell` requirement of the second parameter to be a column number (as described in [README.md](https://github.com/warpech/jquery-handsontable) since always). If you happen to experience an error in `setDataAtCell` after upgrade, change your usage of this method to the new method `setDataAtRowProp` ([#284](https://github.com/warpech/jquery-handsontable/issues/284), [90e1b67](https://github.com/warpech/jquery-handsontable/commit/90e1b6765cf3aa6e0e5c5a9156b9d45da74fa055)) -- new methods `setDataAtRowProp` and `getDataAtRowProp` that set/get data according to the property name in data source. See [README.md](https://github.com/warpech/jquery-handsontable) for clarification ([#284](https://github.com/warpech/jquery-handsontable/issues/284), [90e1b67](https://github.com/warpech/jquery-handsontable/commit/90e1b6765cf3aa6e0e5c5a9156b9d45da74fa055)) -- merged pull request [#266](https://github.com/warpech/jquery-handsontable/pull/266) - fix countCols for arrays with column settings - -## [0.8.10](https://github.com/warpech/jquery-handsontable/tree/v0.8.10) (Mar 7, 2013) - -Bugfix: -- inline cell styles applied by cell renderer were not removed on table scroll ([#353](https://github.com/warpech/jquery-handsontable/issues/353), [#376](https://github.com/warpech/jquery-handsontable/issues/376), [a9b328d](https://github.com/warpech/jquery-handsontable/commit/a9b328d2fcbcb7e7a05f1d2494a1f9062bd17a37)) - -Docs: -- simplify CSS for demo pages (get rid of `bottomSpace` classes) -- refresh the style of jsFiddle links - -## [0.8.9](https://github.com/warpech/jquery-handsontable/tree/v0.8.9) (Mar 6, 2013) - -This is a feature release of a new cell type. For people waiting for some bug fixes, I promise next release will focus on fixing some important bugs! - -Features: -- **Date cell type**. Updated pages [Date](http://handsontable.com/demo/date.html) and [Cell types](http://handsontable.com/demo/renderers.html). Solves issue [#431](https://github.com/warpech/jquery-handsontable/issues/431) -- refactored the `text`, `autocomplete` and `date` cell editors to share as much code as possible - -## [0.8.8](https://github.com/warpech/jquery-handsontable/tree/v0.8.8) (Mar 4, 2013) - -Bugfixes: -- `startRows`/`startCols` option did not work as described in README.md. This parameters should be only taken into account when NO data source is provided ([5c865ba](https://github.com/warpech/jquery-handsontable/commit/5c865ba15dcb7d9f93a47b14aedfc96751bcbb7a)) -- finish editing should move the focus aways from textarea to table cell ([f4f6496](https://github.com/warpech/jquery-handsontable/commit/f4f649600311ba527c19dc2e8f73af2e764c54d6)) - -## [0.8.7](https://github.com/warpech/jquery-handsontable/tree/v0.8.7) (Mar 1, 2013) - -Bugfixes: -- use default cell editor for a cell that has declared only cell renderer ([#453](https://github.com/warpech/jquery-handsontable/issues/453), [a5c61a2](https://github.com/warpech/jquery-handsontable/commit/a5c61a26b9e833abd25afa31aee4199cc6810590)) -- copy/paste did not work on Mac since 0.8.6 ([#348](https://github.com/warpech/jquery-handsontable/issues/348), [463d5c9](https://github.com/warpech/jquery-handsontable/commit/463d5c918b85b2dc74df2669047134c23ae15fcc)) -- global shortcuts (like CTRL+A) should be blocked when cell is being edited ([8363008](https://github.com/warpech/jquery-handsontable/commit/8363008c3756ea869f50b2ad8a213839a50ad807)) -- numeric cell editor did not convert cell data to number type when data source was an object ([b09f6d3](https://github.com/warpech/jquery-handsontable/commit/b09f6d3666d238ac0ae849937152baffa6fb2824)) -- another attempt to solve scrolling to the top of table on any key press if the top is not on the screen ([#348](https://github.com/warpech/jquery-handsontable/issues/348), [d37859b](https://github.com/warpech/jquery-handsontable/commit/d37859b6acdaef0be8b886b33404144c7cf21227)) - -## [0.8.6](https://github.com/warpech/jquery-handsontable/tree/v0.8.6) (Feb 27, 2013) - -Features: -- **Numeric cell type**. Updated pages [Numeric](http://handsontable.com/demo/numeric.html) and [Column sorting](http://handsontable.com/demo/sorting.html). Solves issues [#443](https://github.com/warpech/jquery-handsontable/issues/443), [#397](https://github.com/warpech/jquery-handsontable/issues/397), [#336](https://github.com/warpech/jquery-handsontable/issues/336). Partially solves issue [#121](https://github.com/warpech/jquery-handsontable/issues/121) ([a052013](https://github.com/warpech/jquery-handsontable/commit/a052013049ccf6ca75a7104ddeaaec2f84961f93)) -- autoColumnSize feature is now aware of renderer functions (before this, cells rendered with autocomplete and numeric renderer were too narrow) ([7770f8a](https://github.com/warpech/jquery-handsontable/commit/7770f8a499e5c21891308bdb4d1111bfc79dd2dc)) - -Bugfix: -- scrolls to top of table on any key press if the top is not on the screen ([#348](https://github.com/warpech/jquery-handsontable/issues/348), [290cde9](https://github.com/warpech/jquery-handsontable/commit/290cde93503686f1a89e5533662e3c9ca80d880e), [408f29d](https://github.com/warpech/jquery-handsontable/commit/408f29d6a65e797572adf45873780c3adfc0bf4d)) -- cell editor was using a wrong cell value if column order was manually changed ([#367](https://github.com/warpech/jquery-handsontable/issues/367)) -- cell editor dimensions were not refreshed if table was rerendered during editing -- fix recalculation of container dimensions after window resize ([#400](https://github.com/warpech/jquery-handsontable/issues/400), [#428](https://github.com/warpech/jquery-handsontable/issues/428)) - -Other: -- reimplement cell editing abstraction. Now TextEditor `' + - '' + - '' + - ''); - form.css({ - visibility: 'hidden' - }); - $('body').append(form); - form.submit(); - form.remove(); - }); - - bindDumpButton(); - }); - - function bindDumpButton() { - $('body').on('click', 'button[name=dump]', function () { - var dump = $(this).data('dump'); - var $container = $(dump); - console.log('data of ' + dump, $container.handsontable('getData')); - }); - } -})(jQuery); \ No newline at end of file diff --git a/bower_components/handsontable/demo/json/autocomplete.json b/bower_components/handsontable/demo/json/autocomplete.json deleted file mode 100644 index c5e1bae1..00000000 --- a/bower_components/handsontable/demo/json/autocomplete.json +++ /dev/null @@ -1 +0,0 @@ -["Acura","Audi","BMW","Buick","Cadillac","Chevrolet","Chrysler","Citroen","Dodge","Eagle","Ferrari","Ford","General Motors","GMC","Honda","Hummer","Hyundai","Infiniti","Isuzu","Jaguar","Jeep","Kia","Lamborghini","Land Rover","Lexus","Lincoln","Lotus","Mazda","Mercedes-Benz","Mercury","Mitsubishi","Nissan","Oldsmobile","Peugeot","Pontiac","Porsche","Regal","Renault","Saab","Saturn","Seat","Skoda","Subaru","Suzuki","Toyota","Volkswagen","Volvo"] \ No newline at end of file diff --git a/bower_components/handsontable/demo/json/load.json b/bower_components/handsontable/demo/json/load.json deleted file mode 100644 index 33c464ac..00000000 --- a/bower_components/handsontable/demo/json/load.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "data": [ - ["", "Kia", "Nissan", "Toyota", "Honda"], - ["2008", 10, 11, 12, 13], - ["2009", 20, 11, 14, 13], - ["2010", 30, 15, 12, 13] - ] -} \ No newline at end of file diff --git a/bower_components/handsontable/demo/json/save.json b/bower_components/handsontable/demo/json/save.json deleted file mode 100644 index 45c565f8..00000000 --- a/bower_components/handsontable/demo/json/save.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "result": "ok" -} \ No newline at end of file diff --git a/bower_components/handsontable/demo/legend.html b/bower_components/handsontable/demo/legend.html deleted file mode 100644 index a2326c0e..00000000 --- a/bower_components/handsontable/demo/legend.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - Legend - Handsontable - - - - - -

    Legend was removed in Handsontable 0.7.0

    - -

    Instead, please use cell renderers. Also see conditional formatting tips

    - - - \ No newline at end of file diff --git a/bower_components/handsontable/demo/numeric.html b/bower_components/handsontable/demo/numeric.html deleted file mode 100644 index 413ece03..00000000 --- a/bower_components/handsontable/demo/numeric.html +++ /dev/null @@ -1,161 +0,0 @@ - - - - - Numeric cell type - Handsontable - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Fork me on GitHub - -
    -
    - -
    -
    -
    -

    Handsontable

    - -
    a minimalistic Excel-like data grid editor - for HTML, JavaScript & jQuery -
    -
    -
    -
    - -
    -
    -
    - - -

    Numeric cell type

    - -

    By default, Handsontable treats all cell values as string type. This is because <textarea> - returns a string as its value.

    - -

    In many cases you will prefer cell values to be treated as number type. This allows to format - numbers nicely and sort them correctly.

    - -

    To trigger the Numeric cell type, use the option type: 'numeric' in columns array - or - cells function.

    - -

    Numeric cell type uses Numeral.js as the formatting library. Head over - to - their website to learn about the formatting syntax.

    - -

    To use number formatting style valid for your language (i18n), load language definition to Numeral.js. See - "Languages" section in Numeral.js docs for more info.

    - -
    - -

    - -

    -
    -
    - -
    -
    -
    - -
    - - -
    -
    -
    - -
    -
    -

    For more examples, head back to the main page.

    - -

    Handsontable © 2012 Marcin Warpechowski and contributors.
    Code and documentation - licensed under the The MIT License.

    -
    -
    -
    -
    -
    - - \ No newline at end of file diff --git a/bower_components/handsontable/demo/options.html b/bower_components/handsontable/demo/options.html deleted file mode 100644 index a7f21622..00000000 --- a/bower_components/handsontable/demo/options.html +++ /dev/null @@ -1,165 +0,0 @@ - - - - - Options - Handsontable - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Fork me on GitHub - -
    -
    - -
    -
    -
    -

    Handsontable

    - -
    a minimalistic Excel-like data grid editor - for HTML, JavaScript & jQuery -
    -
    -
    -
    - -
    -
    -
    - - -

    Options

    - -

    - This demo lets you play with some table options (currently only pasteMode, but more to come). -

    - -

    - Use the drop-down menu under the table to change Handsontable options live. -

    - -
    - -

    - -

    - -

    - -

    - -

    - -

    -
    -
    - -
    -
    -
    - -
    - - -
    -
    -
    - -
    -
    -

    For more examples, head back to the main page.

    - -

    Handsontable © 2012 Marcin Warpechowski and contributors.
    Code and documentation - licensed under the The MIT License.

    -
    -
    -
    -
    -
    - - \ No newline at end of file diff --git a/bower_components/handsontable/demo/pagination.html b/bower_components/handsontable/demo/pagination.html deleted file mode 100644 index 31e1f252..00000000 --- a/bower_components/handsontable/demo/pagination.html +++ /dev/null @@ -1,168 +0,0 @@ - - - - - Pagination - Handsontable - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Fork me on GitHub - - -
    -
    - -
    -
    -
    -

    Handsontable

    - -
    a minimalistic Excel-like data grid editor - for HTML, JavaScript & jQuery -
    -
    -
    -
    - -
    -
    -
    - - -

    Pagination

    - -
    - - - -

    - -

    -
    -
    - -
    -
    -
    - -
    - - - - -
    -
    -
    - -
    -
    -

    For more examples, head back to the main page.

    - -

    Handsontable © 2012 Marcin Warpechowski and contributors.
    Code and documentation - licensed under the The MIT License.

    -
    -
    -
    -
    -
    - - \ No newline at end of file diff --git a/bower_components/handsontable/demo/php.html b/bower_components/handsontable/demo/php.html deleted file mode 100644 index bcef367f..00000000 --- a/bower_components/handsontable/demo/php.html +++ /dev/null @@ -1,210 +0,0 @@ - - - - - PHP example - Handsontable - - - - - - - - - - - - - - - - - - - - - - - - - - - -Fork me on GitHub - -
    -
    - -
    -
    -
    -

    Handsontable

    - -
    a minimalistic Excel-like data grid editor - for HTML, JavaScript & jQuery -
    -
    -
    -
    - -
    -
    -
    -

    PHP example

    - -

    This page loads and saves data on server. In this example, client side uses $.ajax. Server side uses - PHP with PDO (SQLite).

    - -

    Please note. This page and the PHP scripts are a work in progress. They are not yet configured on GitHub. - Please run it on your own localhost.

    - -

    - - - - -

    - -
    Click "Load" to load data from server
    - -
    - -

    - -

    -
    -
    - -
    -
    - -
    -
    -
    - -
    -
    -

    For more examples, head back to the main page.

    - -

    Handsontable © 2012 Marcin Warpechowski and contributors.
    Code and documentation - licensed under the The MIT License.

    -
    -
    -
    -
    -
    - - \ No newline at end of file diff --git a/bower_components/handsontable/demo/php/cars.php b/bower_components/handsontable/demo/php/cars.php deleted file mode 100644 index 324fa78f..00000000 --- a/bower_components/handsontable/demo/php/cars.php +++ /dev/null @@ -1,74 +0,0 @@ - \ No newline at end of file diff --git a/bower_components/handsontable/demo/php/functions.php b/bower_components/handsontable/demo/php/functions.php deleted file mode 100644 index 724aa943..00000000 --- a/bower_components/handsontable/demo/php/functions.php +++ /dev/null @@ -1,78 +0,0 @@ -exec("CREATE TABLE IF NOT EXISTS cars (id INTEGER PRIMARY KEY, manufacturer TEXT, year INTEGER, price INTEGER)"); -} - -function resetCarsTable($db){ - dropCarsTable($db); - createCarsTable($db); - loadDefaultCars($db); -} - -function dropCarsTable($db){ - $db->exec("DROP TABLE IF EXISTS cars"); -} - -function loadDefaultCars($db){ - $query = $db->prepare('INSERT INTO cars (id, manufacturer, year, price) VALUES(:id, :manufacturer, :year, :price)'); - $data = array( - array( - 'id' => 1, - 'manufacturer' => 'Honda', - 'year' => 2010, - 'price' => 200000 - ), - array( - 'id' => 2, - 'manufacturer' => 'Jaguar', - 'year' => 2012, - 'price' => 400000 - ), - array( - 'id' => 3, - 'manufacturer' => 'BMW', - 'year' => 2000, - 'price' => 75000 - ), - array( - 'id' => 4, - 'manufacturer' => 'Mercedes', - 'year' => 1980, - 'price' => 1000 - ) - ); - - foreach($data as $index => $value){ - $query->bindValue(':id', $value['id'], PDO::PARAM_INT); - $query->bindValue(':manufacturer', $value['manufacturer'], PDO::PARAM_STR); - $query->bindValue(':year', $value['year'], PDO::PARAM_INT); - $query->bindValue(':price', $value['price'], PDO::PARAM_INT); - $query->execute(); - } -} - -function &loadCars($db){ - $select = $db->prepare('SELECT * FROM cars ORDER BY id ASC LIMIT 100'); - $select->execute(); - - return $select; -} - -function carsTableExists($db){ - $result = $db->query("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='cars'"); - - $row = $result->fetch(PDO::FETCH_NUM); - - return $row[0] > 0; -} \ No newline at end of file diff --git a/bower_components/handsontable/demo/php/load.php b/bower_components/handsontable/demo/php/load.php deleted file mode 100644 index 22952002..00000000 --- a/bower_components/handsontable/demo/php/load.php +++ /dev/null @@ -1,38 +0,0 @@ - $result->fetchAll(PDO::FETCH_ASSOC) - ); - echo json_encode($out); - - // close the database connection - closeConnection($db); -} -catch (PDOException $e) { - print 'Exception : ' . $e->getMessage(); -} -?> \ No newline at end of file diff --git a/bower_components/handsontable/demo/php/reset.php b/bower_components/handsontable/demo/php/reset.php deleted file mode 100644 index cdb1cbf8..00000000 --- a/bower_components/handsontable/demo/php/reset.php +++ /dev/null @@ -1,14 +0,0 @@ -getMessage(); -} - diff --git a/bower_components/handsontable/demo/php/save.php b/bower_components/handsontable/demo/php/save.php deleted file mode 100644 index f5ba927d..00000000 --- a/bower_components/handsontable/demo/php/save.php +++ /dev/null @@ -1,90 +0,0 @@ - 'manufacturer', - 1 => 'year', - 2 => 'price' - ); - - if (isset($_POST['changes']) && $_POST['changes']) { - foreach ($_POST['changes'] as $change) { - $rowId = $change[0] + 1; - $colId = $change[1]; - $newVal = $change[3]; - - if (!isset($colMap[$colId])) { - echo "\n spadam"; - continue; - } - - $select = $db->prepare('SELECT id FROM cars WHERE id=? LIMIT 1'); - $select->execute(array( - $rowId - )); - - if ($row = $select->fetch()) { - $query = $db->prepare('UPDATE cars SET `' . $colMap[$colId] . '` = :newVal WHERE id = :id'); - } else { - $query = $db->prepare('INSERT INTO cars (id, `' . $colMap[$colId] . '`) VALUES(:id, :newVal)'); - } - $query->bindValue(':id', $rowId, PDO::PARAM_INT); - $query->bindValue(':newVal', $newVal, PDO::PARAM_STR); - $query->execute(); - } - } elseif (isset($_POST['data']) && $_POST['data']) { - $select = $db->prepare('DELETE FROM cars'); - $select->execute(); - - for ($r = 0, $rlen = count($_POST['data']); $r < $rlen; $r++) { - $rowId = $r + 1; - for ($c = 0, $clen = count($_POST['data'][$r]); $c < $clen; $c++) { - if (!isset($colMap[$c])) { - continue; - } - - $newVal = $_POST['data'][$r][$c]; - - $select = $db->prepare('SELECT id FROM cars WHERE id=? LIMIT 1'); - $select->execute(array( - $rowId - )); - - if ($row = $select->fetch()) { - $query = $db->prepare('UPDATE cars SET `' . $colMap[$c] . '` = :newVal WHERE id = :id'); - } else { - $query = $db->prepare('INSERT INTO cars (id, `' . $colMap[$c] . '`) VALUES(:id, :newVal)'); - } - $query->bindValue(':id', $rowId, PDO::PARAM_INT); - $query->bindValue(':newVal', $newVal, PDO::PARAM_STR); - $query->execute(); - } - } - } - - $out = array( - 'result' => 'ok' - ); - echo json_encode($out); - - closeConnection($db); -} -catch (PDOException $e) { - print 'Exception : ' . $e->getMessage(); -} -?> \ No newline at end of file diff --git a/bower_components/handsontable/demo/prepopulate.html b/bower_components/handsontable/demo/prepopulate.html deleted file mode 100644 index 7c07dc72..00000000 --- a/bower_components/handsontable/demo/prepopulate.html +++ /dev/null @@ -1,182 +0,0 @@ - - - - - Pre-populate new rows from template - Handsontable - - - - - - - - - - - - - - - - - - - - - - - - - - - -Fork me on GitHub - -
    -
    - -
    -
    -
    -

    Handsontable

    - -
    a minimalistic Excel-like data grid editor - for HTML, JavaScript & jQuery -
    -
    -
    -
    - -
    -
    -
    -

    Pre-populate new rows from template

    - -

    Below example shows how cell type renderers can be used to present the template values for empty rows.

    - -

    When any cell in the empty row is edited, the - onChange callback fills the row with the template values.

    - -
    - - - -

    - -

    -
    -
    - -
    -
    -
    - -
    - - -
    -
    -
    - -
    -
    -

    For more examples, head back to the main page.

    - -

    Handsontable © 2012 Marcin Warpechowski and contributors.
    Code and documentation - licensed under the The MIT License.

    -
    -
    -
    -
    -
    - - \ No newline at end of file diff --git a/bower_components/handsontable/demo/readonly.html b/bower_components/handsontable/demo/readonly.html deleted file mode 100644 index 28ca7415..00000000 --- a/bower_components/handsontable/demo/readonly.html +++ /dev/null @@ -1,204 +0,0 @@ - - - - - Read-only cells - Handsontable - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Fork me on GitHub - -
    -
    - -
    -
    -
    -

    Handsontable

    - -
    a minimalistic Excel-like data grid editor - for HTML, JavaScript & jQuery -
    - -

    Read-only cells

    - -

    This page shows ways to configure columns or cells to be read only:

    - - -
    -
    -
    - -
    -
    -
    - - -

    Read-only columns

    - -

    In many usage cases, you will need to configure a certain column to be read only. This column will be - available for keyboard navigation and CTRL+C. Only editing and pasting data will be disabled.

    - -

    To make a column read-only, declare it in the columns setting. You can also define a special - renderer function that will dim the read-only values.

    - -
    - -

    - -

    -
    -
    - -
    -
    -
    - -
    - - -
    -
    -
    - -
    -
    -
    - - -

    Read-only specific cells

    - -

    This example makes cells that contain the word "Nissan" read only.

    - -

    It forces all cells to be rendered by myReadonlyRenderer, which will decide wheather a cell is - really read only by checking its readOnly property.

    - -
    - -

    - -

    -
    -
    - -
    -
    -
    - -
    - - -
    -
    -
    - -
    -
    -

    For more examples, head back to the main page.

    - -

    Handsontable © 2012 Marcin Warpechowski and contributors.
    Code and documentation - licensed under the The MIT License.

    -
    -
    -
    -
    -
    - - \ No newline at end of file diff --git a/bower_components/handsontable/demo/renderers.html b/bower_components/handsontable/demo/renderers.html deleted file mode 100644 index 88de4806..00000000 --- a/bower_components/handsontable/demo/renderers.html +++ /dev/null @@ -1,213 +0,0 @@ - - - - - Cell types - Handsontable - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Fork me on GitHub - -
    -
    - -
    -
    -
    -

    Handsontable

    - -
    a minimalistic Excel-like data grid editor - for HTML, JavaScript & jQuery -
    - -

    Cell types

    - -

    This page is an introduction to Handsontable cell types:

    - - -
    -
    -
    - -
    -
    -
    - - -

    Preview of built-in and custom cell types

    - -

    The below example shows all built-in cell types (in other words, combinations of cell renderers and - editors) - available in Handsontable:

    - - - -

    The same example also shows the declaration of custom cell renderers, namely yellowRenderer - and - greenRenderer. -

    - -
    - -

    - -

    -
    -
    - -
    -
    -
    - -
    - - -
    -
    -
    - -
    -
    -
    - - -

    Anatomy of a cell type

    - -

    A cell type is a predefined set of cell properties. Cell type defines what renderer - and editor should be used for a cell. They can also define any different cell property that - will be assumed for each matching cell.

    - -

    For example writing:

    - -
    
    -          columns: [
    -          {
    -          type: 'text'
    -          }
    -          ]
    -        
    - -

    Equals:

    - -
    
    -          columns: [
    -          {
    -          renderer: Handsontable.TextRenderer,
    -          editor: Handsontable.TextEditor
    -          }
    -          ]
    -        
    - - This mapping is defined in file src/cellTypes.js -
    -
    -
    - -
    -
    -

    For more examples, head back to the main page.

    - -

    Handsontable © 2012 Marcin Warpechowski and contributors.
    Code and documentation - licensed under the The MIT License.

    -
    -
    -
    -
    -
    - - \ No newline at end of file diff --git a/bower_components/handsontable/demo/renderers_html.html b/bower_components/handsontable/demo/renderers_html.html deleted file mode 100644 index a1aa06d3..00000000 --- a/bower_components/handsontable/demo/renderers_html.html +++ /dev/null @@ -1,411 +0,0 @@ - - - - - Custom HTML in cells and headers - Handsontable - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Fork me on GitHub - -
    -
    - -
    -
    -
    -

    Handsontable

    - -
    a minimalistic Excel-like data grid editor - for HTML, JavaScript & jQuery -
    - -

    Custom HTML in cells and headers

    - -

    On this page:

    - - -
    -
    -
    - -
    -
    -
    - - -

    Rendering custom HTML in cells

    - -

    This example shows how to use custom cell renderers to display HTML content in a cell.

    - -

    This is a very powerful feature. Just remember to escape any HTML code that could be used for XSS - attacks.

    - -
    - -

    - -

    -
    -
    - -
    -
    -
    - -
    - - -
    -
    -
    - -
    -
    -
    - - -

    Rendering custom HTML in header

    - -

    You can also put HTML into row and column headers.

    - -

    If you need to attach events to DOM elements like the checkbox below, just remember to identify the element - by class name, not by id. This is because row and column headers are duplicated in the DOM tree and id - attribute must be unique.

    - -
    - -

    - -

    -
    -
    - -
    -
    -
    - -
    - - -
    -
    -
    - -
    -
    -
    - - -

    Changing cell type from a dropdown menu in cell header

    - -

    This example makes use of a plugin hook to add a custom dropdown menu to the cell header

    - -
    - -

    - -

    -
    -
    - -
    -
    -
    - -
    - - - - -
    -
    -
    - -
    -
    -

    For more examples, head back to the main page.

    - -

    Handsontable © 2012 Marcin Warpechowski and contributors.
    Code and documentation - licensed under the The MIT License.

    -
    -
    -
    -
    -
    - - \ No newline at end of file diff --git a/bower_components/handsontable/demo/scroll.html b/bower_components/handsontable/demo/scroll.html deleted file mode 100644 index 6b080f51..00000000 --- a/bower_components/handsontable/demo/scroll.html +++ /dev/null @@ -1,332 +0,0 @@ - - - - - Scroll - Handsontable - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Fork me on GitHub - -
    -
    - -
    -
    -
    -

    Handsontable

    - -
    a minimalistic Excel-like data grid editor - for HTML, JavaScript & jQuery -
    - -

    Scrollbars

    - -

    This page shows how to configure Handsontable scrollbars:

    - - -
    -
    -
    - -
    -
    -
    - - -

    Vertical and horizontal scrollbars

    - -

    If you want scrollbars, just set container width, height and overflow: scroll in CSS.

    - -

    - -

    - -
    - -

    - -

    -
    -
    - -
    -
    -
    - -
    - - -
    -
    -
    - -
    -
    -
    - - -

    Single scrollbar (stretch last column)

    - -

    It is also possible to configure only a single scrollbar. The following example creates one by specifying - only the container height and overflow: auto in CSS.

    - -
    - -

    - -

    -
    -
    - -
    -
    -
    - -
    - - -
    -
    -
    - -
    -
    -
    - - -

    Single scrollbar (stretch all columns)

    - -

    If the table content is not as wide as the container width, the table will be stretched to the container - width. The default horizontal stretch model is to stretch the last column only (by using stretchH: - 'last' option).

    - -

    Other possible stretch modes are all (stretches all columns equally, used in the below example) - and none (not stretching).

    - -
    - -

    - -

    -
    -
    - -
    -
    -
    - -
    - - -
    -
    -
    - -
    -
    -
    - - -

    Single scrollbar (hybrid mode - default)

    - -

    Sometimes you don't know if horizontal scroll bar will be needed. That's why stretchH: 'hybrid' is - the default column stretching mode

    - -

    When horizontal scrollbar is NOT visible, it acts as stretchH: 'none'. When horizontal scrollbar - is visible, it acts as stretchH: 'last'.

    - -
    - -

    - -

    -
    -
    - -
    -
    -
    - -
    - - -
    -
    -
    - -
    -
    -

    For more examples, head back to the main page.

    - -

    Handsontable © 2012 Marcin Warpechowski and contributors.
    Code and documentation - licensed under the The MIT License.

    -
    -
    -
    -
    -
    - - \ No newline at end of file diff --git a/bower_components/handsontable/demo/scroll_native.html b/bower_components/handsontable/demo/scroll_native.html deleted file mode 100644 index 9f290998..00000000 --- a/bower_components/handsontable/demo/scroll_native.html +++ /dev/null @@ -1,143 +0,0 @@ - - - - - Native Scrollbar - Handsontable - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Fork me on GitHub - -
    -
    - -
    -
    -
    -

    Handsontable

    - -
    a minimalistic Excel-like data grid editor - for HTML, JavaScript & jQuery -
    -
    -
    -
    - -
    -
    -
    - - -

    Native Scrollbar

    - -

    This demo shows table of 100 000 rows. Only visible part is rendered. Native window scrollbar is used to - scroll through the table.

    - -

    This feature is considered experimental - not for production use. Normally you should use - the - built-in scrollbars that come with Handsontable. -

    - -

    Currently this feature only works with vertical scrollbar and does NOT work in of a DIV that has overflow: - scroll

    - -

    This feature will continue to be developed. Comments are welcome.

    - -
    - -

    - -

    -
    -
    - -
    -
    -
    - -
    - - -
    -
    -
    - -
    -
    -

    For more examples, head back to the main page.

    - -

    Handsontable © 2012 Marcin Warpechowski and contributors.
    Code and documentation - licensed under the The MIT License.

    -
    -
    -
    -
    -
    - - \ No newline at end of file diff --git a/bower_components/handsontable/demo/search.html b/bower_components/handsontable/demo/search.html deleted file mode 100644 index e9809b34..00000000 --- a/bower_components/handsontable/demo/search.html +++ /dev/null @@ -1,150 +0,0 @@ - - - - - Search - Handsontable - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Fork me on GitHub - - -
    -
    - -
    -
    -
    -

    Handsontable

    - -
    a minimalistic Excel-like data grid editor - for HTML, JavaScript & jQuery -
    -
    -
    -
    - -
    -
    -
    - - -

    Search

    - - - -
    - -

    - -

    -
    -
    - -
    -
    -
    - -
    - - - - -
    -
    -
    - -
    -
    -

    For more examples, head back to the main page.

    - -

    Handsontable © 2012 Marcin Warpechowski and contributors.
    Code and documentation - licensed under the The MIT License.

    -
    -
    -
    -
    -
    - - \ No newline at end of file diff --git a/bower_components/handsontable/demo/sorting.html b/bower_components/handsontable/demo/sorting.html deleted file mode 100644 index 432ff2a3..00000000 --- a/bower_components/handsontable/demo/sorting.html +++ /dev/null @@ -1,178 +0,0 @@ - - - - - Column sorting - Handsontable - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Fork me on GitHub - -
    -
    - -
    -
    -
    -

    Handsontable

    - -
    a minimalistic Excel-like data grid editor - for HTML, JavaScript & jQuery -
    -
    -
    -
    - -
    -
    -
    - - -

    Column sorting

    - -

    To enable this experimental feature, use setting columnSorting: true

    - -

    Click on a column header to sort.

    - -

    Only the table view is sorted. The data source remains in the original order.

    - -
    State of the table has been restored.
    - -
    - -

    - -

    -
    -
    - -
    -
    -
    - -
    - - -
    -
    -
    - -
    -
    -

    For more examples, head back to the main page.

    - -

    Handsontable © 2012 Marcin Warpechowski and contributors.
    Code and documentation - licensed under the The MIT License.

    -
    -
    -
    -
    -
    - - \ No newline at end of file diff --git a/bower_components/handsontable/demo/understanding_reference.html b/bower_components/handsontable/demo/understanding_reference.html deleted file mode 100644 index 54bcbcf2..00000000 --- a/bower_components/handsontable/demo/understanding_reference.html +++ /dev/null @@ -1,189 +0,0 @@ - - - - - Understanding binding as reference - Handsontable - - - - - - - - - - - - - - - - - - - - - - - - - - - -Fork me on GitHub - -
    -
    - -
    -
    -
    -

    Handsontable

    - -
    a minimalistic Excel-like data grid editor - for HTML, JavaScript & jQuery -
    - -

    Understanding binding as reference

    -
    -
    -
    - -
    -
    -
    -

    Handsontable binds to your data source (array or object) by reference. Therefore, all the data entered in - the - grid will alter the original data source.

    - -

    In complex applications, you may have a purpose to change data source programatically (outside of - Handsontable). A value change that was done programatically will not be presented on the screen unless you - refresh the grid on screen using the render method.

    - -
    - -

    - -

    -
    -
    - -
    -
    - -
    -
    -
    - -
    -
    -
    -

    But I want to change my data without rendering changes!

    -
    -
    -
    - -
    -
    -
    -

    In case you want to keep separate working copy of data for Handsontable, it is suggested to clone the data - source before you load it to Handsontable.

    - -

    This can easily be done with $.extend method.

    -
    -
    - -
    -
    - -
    -
    -
    - -
    -
    -
    -

    In a similar way, you may find it useful to clone data before saving it.

    - -

    That would be useful to make programmatic changes that would be saved to server but kept not invisible to - the - user.

    -
    -
    - -
    -
    - -
    -
    -
    - -
    -
    -

    For more examples, head back to the main page.

    - -

    Handsontable © 2012 Marcin Warpechowski and contributors.
    Code and documentation - licensed under the The MIT License.

    -
    -
    -
    -
    -
    - - \ No newline at end of file diff --git a/bower_components/handsontable/demo/validation.html b/bower_components/handsontable/demo/validation.html deleted file mode 100644 index ebc0eaa0..00000000 --- a/bower_components/handsontable/demo/validation.html +++ /dev/null @@ -1,179 +0,0 @@ - - - - - Validation - Handsontable - - - - - - - - - - - - - - - - - - - - - - - - - - - -Fork me on GitHub - -
    -
    - -
    -
    -
    -

    Handsontable

    - -
    a minimalistic Excel-like data grid editor - for HTML, JavaScript & jQuery -
    -
    -
    -
    - -
    -
    -
    -

    Validation

    - -

    Use the validator (see Options wiki page) method to easily - validate synchronous or asynchronous changes to a cell. If you - need more control, beforeValidate and afterValidate plugin hooks are available (see Events wiki page). -

    - -

    In the below example, email_validator_fn is an async validator that resolves after 1000 ms.

    - -

    Use the allowInvalid option (see Options wiki page) to define if the - grid should accept input that does not validate.

    - -

    - If you need to modify the input (e.g. censor bad words, uppercase first letter), use the plugin hook beforeChange - (see Events wiki page). -

    - -
    - -

    Callback console: [[row, col, oldValue, newValue], ...]

    - -
    Edit the above grid to see callback
    - -

    - -

    -
    -
    - -
    -
    -
    - -
    - - -
    -
    -
    - -
    -
    -

    For more examples, head back to the main page.

    - -

    Handsontable © 2012 Marcin Warpechowski and contributors.
    Code and documentation - licensed under the The MIT License.

    -
    -
    -
    -
    -
    - - \ No newline at end of file diff --git a/bower_components/handsontable/demo/web_component.html b/bower_components/handsontable/demo/web_component.html deleted file mode 100644 index fdf7d550..00000000 --- a/bower_components/handsontable/demo/web_component.html +++ /dev/null @@ -1,319 +0,0 @@ - - - - - Web Component - Handsontable - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Fork me on GitHub - -
    -
    - -
    -
    -
    -

    Handsontable

    - -
    a minimalistic Excel-like data grid editor - for HTML, JavaScript & jQuery -
    - -

    Web Component

    - -

    This page shows our effort to make a Web Component-compatible version of Handsontable.

    - -

    Our Web Component implementation leverages Toolkitchen Toolkit library. Currently, it works correctly - only in Google Chrome. According to the browser compatibility matrix, it should also - be possible to run it in Firefox, IE10, Safari (though right now it produces Assertion failed error - in every browser except Chrome). -

    - -

    All your help (review, comments, pull requests) in this area is very welcome. Please use the GitHub Issues - ticket #603 to share your - thoughts.

    -
    -
    -
    - - - - - - -
    -
    -

    For more examples, head back to the main page.

    - -

    Handsontable © 2012 Marcin Warpechowski and contributors.
    Code and documentation - licensed under the The MIT License.

    -
    -
    -
    -
    -
    - - \ No newline at end of file diff --git a/bower_components/handsontable/demo/web_component/polymer/polymer.min.js b/bower_components/handsontable/demo/web_component/polymer/polymer.min.js deleted file mode 100644 index 89a05346..00000000 --- a/bower_components/handsontable/demo/web_component/polymer/polymer.min.js +++ /dev/null @@ -1,5 +0,0 @@ -function PointerGestureEvent(e,t){var n=t||{},r=document.createEvent("Event"),i={bubbles:!0,cancelable:!0};return Object.keys(i).forEach(function(e){e in n&&(i[e]=n[e])}),r.initEvent(e,i.bubbles,i.cancelable),Object.keys(n).forEach(function(e){r[e]=t[e]}),r.preventTap=this.preventTap,r}if(window.Platform=window.Platform||{},window.logFlags=window.logFlags||{},function(e){var t=e.flags||{};location.search.slice(1).split("&").forEach(function(e){e=e.split("="),e[0]&&(t[e[0]]=e[1]||!0)}),t.shadow=(t.shadowdom||t.shadow||t.polyfill||!HTMLElement.prototype.webkitCreateShadowRoot)&&"polyfill",e.flags=t}(Platform),"polyfill"===Platform.flags.shadow){var SideTable;"undefined"!=typeof WeakMap&&0>navigator.userAgent.indexOf("Firefox/")?SideTable=WeakMap:function(){var e=Object.defineProperty,t=Object.hasOwnProperty,n=(new Date).getTime()%1e9;SideTable=function(){this.name="__st"+(1e9*Math.random()>>>0)+(n++ +"__")},SideTable.prototype={set:function(t,n){e(t,this.name,{value:n,writable:!0})},get:function(e){return t.call(e,this.name)?e[this.name]:void 0},"delete":function(e){this.set(e,void 0)}}}();var ShadowDOMPolyfill={};(function(e){"use strict";function t(e){if(!e)throw Error("Assertion failed")}function n(e,t){return Object.getOwnPropertyNames(t).forEach(function(n){Object.defineProperty(e,n,Object.getOwnPropertyDescriptor(t,n))}),e}function r(e,t){return Object.getOwnPropertyNames(t).forEach(function(n){switch(n){case"arguments":case"caller":case"length":case"name":case"prototype":case"toString":return}Object.defineProperty(e,n,Object.getOwnPropertyDescriptor(t,n))}),e}function i(e){var t=e.__proto__||Object.getPrototypeOf(e),n=S.get(t);if(n)return n;var r=i(t),o=h(r);return u(t,o,e),o}function o(e,t){s(e,t,!0)}function a(e,t){s(t,e,!1)}function s(e,t,n){Object.getOwnPropertyNames(e).forEach(function(r){if(!(r in t)){O&&e.__lookupGetter__(r);var i;try{i=Object.getOwnPropertyDescriptor(e,r)}catch(o){i=C}var a,s;if(n&&"function"==typeof i.value)return t[r]=function(){return this.impl[r].apply(this.impl,arguments)},void 0;a=function(){return this.impl[r]},(i.writable||i.set)&&(s=function(e){this.impl[r]=e}),Object.defineProperty(t,r,{get:a,set:s,configurable:i.configurable,enumerable:i.enumerable})}})}function l(e,t,n){var i=e.prototype;u(i,t,n),r(t,e)}function u(e,n,r){var i=n.prototype;t(void 0===S.get(e)),S.set(e,n),o(e,i),r&&a(i,r)}function c(e,t){return S.get(t.prototype)===e}function d(e){var t=Object.getPrototypeOf(e),n=i(t),r=h(n);return u(t,r,e),r}function h(e){function t(t){e.call(this,t)}return t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t}function p(e){return e instanceof P.EventTarget||e instanceof P.Event||e instanceof P.DOMImplementation}function f(e){return e instanceof N||e instanceof _||e instanceof D||e instanceof L}function v(e){if(null===e)return null;t(f(e));var n=M.get(e);if(!n){var r=i(e);n=new r(e),M.set(e,n)}return n}function m(e){return null===e?null:(t(p(e)),e.impl)}function g(e){return e&&p(e)?m(e):e}function b(e){return e&&!p(e)?v(e):e}function y(e,n){null!==n&&(t(f(e)),t(void 0===n||p(n)),M.set(e,n))}function w(e,t,n){Object.defineProperty(e.prototype,t,{get:n,configurable:!0,enumerable:!0})}function E(e,t){w(e,t,function(){return v(this.impl[t])})}function T(e,t){e.forEach(function(e){t.forEach(function(t){e.prototype[t]=function(){var e=v(this);return e[t].apply(e,arguments)}})})}var M=new SideTable,S=new SideTable,P=Object.create(null);Object.getOwnPropertyNames(window);var O=/Firefox/.test(navigator.userAgent),C={get:function(){},set:function(){},configurable:!0,enumerable:!0},L=DOMImplementation,_=Event,N=Node,D=Window;e.assert=t,e.defineGetter=w,e.defineWrapGetter=E,e.forwardMethodsToWrapper=T,e.isWrapperFor=c,e.mixin=n,e.registerObject=d,e.registerWrapper=l,e.rewrap=y,e.unwrap=m,e.unwrapIfNeeded=g,e.wrap=v,e.wrapIfNeeded=b,e.wrappers=P})(this.ShadowDOMPolyfill),function(e){"use strict";function t(e){return e instanceof k.ShadowRoot}function n(e){var t=e.localName;return"content"===t||"shadow"===t}function r(e){return!!e.shadowRoot}function i(e){var t;return e.parentNode||(t=e.defaultView)&&H(t)||null}function o(o,a,s){if(s.length)return s.shift();if(t(o))return o.insertionParent||e.getHostForShadowRoot(o);var l=e.eventParentsTable.get(o);if(l){for(var u=1;l.length>u;u++)s[u-1]=l[u];return l[0]}if(a&&n(o)){var c=o.parentNode;if(c&&r(c))for(var d=e.getShadowTrees(c),h=a.insertionParent,u=0;d.length>u;u++)if(d[u].contains(h))return h}return i(o)}function a(e){for(var r=[],i=e,a=[],l=[];i;){var u=null;if(n(i)){u=s(r);var c=r[r.length-1]||i;r.push(c)}else r.length||r.push(i);var d=r[r.length-1];a.push({target:d,currentTarget:i}),t(i)&&r.pop(),i=o(i,u,l)}return a}function s(e){for(var t=e.length-1;t>=0;t--)if(!n(e[t]))return e[t];return null}function l(r,i){for(var a=[];r;){for(var l=[],c=i,h=void 0;c;){var p=null;if(l.length){if(n(c)&&(p=s(l),u(h))){var f=l[l.length-1];l.push(f)}}else l.push(c);if(d(c,r))return l[l.length-1];t(c)&&l.pop(),h=c,c=o(c,p,a)}r=t(r)?e.getHostForShadowRoot(r):r.parentNode}}function u(e){return e.insertionParent}function c(e){for(var t;t=e.parentNode;)e=t;return e}function d(e,t){return c(e)===c(t)}function h(e){switch(e){case"DOMAttrModified":case"DOMAttributeNameChanged":case"DOMCharacterDataModified":case"DOMElementNameChanged":case"DOMNodeInserted":case"DOMNodeInsertedIntoDocument":case"DOMNodeRemoved":case"DOMNodeRemovedFromDocument":case"DOMSubtreeModified":return!0}return!1}function p(t){if(!I.get(t)){I.set(t,!0),h(t.type)||e.renderAllPending();var n=H(t.target),r=H(t);return f(r,n)}}function f(e,t){var n=a(t);return"load"===e.type&&2===n.length&&n[0].target instanceof k.Document&&n.shift(),v(e,n)&&m(e,n)&&g(e,n),B.set(e,w.NONE),F.set(e,null),e.defaultPrevented}function v(e,t){for(var n,r=t.length-1;r>0;r--){var i=t[r].target,o=t[r].currentTarget;if(i!==o&&(n=w.CAPTURING_PHASE,!b(t[r],e,n)))return!1}return!0}function m(e,t){var n=w.AT_TARGET;return b(t[0],e,n)}function g(e,t){for(var n,r=e.bubbles,i=1;t.length>i;i++){var o=t[i].target,a=t[i].currentTarget;if(o===a)n=w.AT_TARGET;else{if(!r||Y.get(e))continue;n=w.BUBBLING_PHASE}if(!b(t[i],e,n))return}}function b(e,t,n){var r=e.target,i=e.currentTarget,o=R.get(i);if(!o)return!0;if("relatedTarget"in t){var a=x(t),s=H(a.relatedTarget),u=l(i,s);if(u===r)return!0;U.set(t,u)}B.set(t,n);var c=t.type,d=!1;j.set(t,r),F.set(t,i);for(var h=0;o.length>h;h++){var p=o[h];if(p.removed)d=!0;else if(!(p.type!==c||!p.capture&&n===w.CAPTURING_PHASE||p.capture&&n===w.BUBBLING_PHASE))try{if("function"==typeof p.handler?p.handler.call(i,t):p.handler.handleEvent(t),Y.get(t))return!1}catch(f){window.onerror?window.onerror(f.message):console.error(f)}}if(d){var v=o.slice();o.length=0;for(var h=0;v.length>h;h++)v[h].removed||o.push(v[h])}return!q.get(t)}function y(e,t,n){this.type=e,this.handler=t,this.capture=Boolean(n)}function w(e,t){return e instanceof W?(this.impl=e,void 0):H(S(W,"Event",e,t))}function E(e){return e&&e.relatedTarget?Object.create(e,{relatedTarget:{value:x(e.relatedTarget)}}):e}function T(e,t,n){var r=window[e],i=function(t,n){return t instanceof r?(this.impl=t,void 0):H(S(r,e,t,n))};return i.prototype=Object.create(t.prototype),n&&D(i.prototype,n),r&&A(r,i,document.createEvent(e)),i}function M(e,t){return function(){arguments[t]=x(arguments[t]);var n=x(this);n[e].apply(n,arguments)}}function S(e,t,n,r){if(et)return new e(n,E(r));var i=x(document.createEvent(t)),o=Z[t],a=[n];return Object.keys(o).forEach(function(e){var t=null!=r&&e in r?r[e]:o[e];"relatedTarget"===e&&(t=x(t)),a.push(t)}),i["init"+t].apply(i,a),i}function P(e){return"function"==typeof e?!0:e&&e.handleEvent}function O(e){this.impl=e}function C(t){return t instanceof k.ShadowRoot&&(t=e.getHostForShadowRoot(t)),x(t)}function L(e){N(e,rt)}function _(t,n,r,i){e.renderAllPending();for(var o=H(it.call(n.impl,r,i)),s=a(o,this),l=0;s.length>l;l++){var u=s[l];if(u.currentTarget===t)return u.target}return null}var N=e.forwardMethodsToWrapper,D=e.mixin,A=e.registerWrapper,x=e.unwrap,H=e.wrap,k=e.wrappers;new SideTable;var R=new SideTable,I=new SideTable,j=new SideTable,F=new SideTable,U=new SideTable,B=new SideTable,q=new SideTable,Y=new SideTable;y.prototype={equals:function(e){return this.handler===e.handler&&this.type===e.type&&this.capture===e.capture},get removed(){return null===this.handler},remove:function(){this.handler=null}};var W=window.Event;w.prototype={get target(){return j.get(this)},get currentTarget(){return F.get(this)},get eventPhase(){return B.get(this)},stopPropagation:function(){q.set(this,!0)},stopImmediatePropagation:function(){q.set(this,!0),Y.set(this,!0)}},A(W,w,document.createEvent("Event"));var V=T("UIEvent",w),G=T("CustomEvent",w),X={get relatedTarget(){return U.get(this)||H(x(this).relatedTarget)}},z=D({initMouseEvent:M("initMouseEvent",14)},X),K=D({initFocusEvent:M("initFocusEvent",5)},X),Q=T("MouseEvent",V,z),$=T("FocusEvent",V,K),J=T("MutationEvent",w,{initMutationEvent:M("initMutationEvent",3),get relatedNode(){return H(this.impl.relatedNode)}}),Z=Object.create(null),et=function(){try{new window.MouseEvent("click")}catch(e){return!1}return!0}();if(!et){var tt=function(e,t,n){if(n){var r=Z[n];t=D(D({},r),t)}Z[e]=t};tt("Event",{bubbles:!1,cancelable:!1}),tt("CustomEvent",{detail:null},"Event"),tt("UIEvent",{view:null,detail:0},"Event"),tt("MouseEvent",{screenX:0,screenY:0,clientX:0,clientY:0,ctrlKey:!1,altKey:!1,shiftKey:!1,metaKey:!1,button:0,relatedTarget:null},"UIEvent"),tt("FocusEvent",{relatedTarget:null},"UIEvent")}var nt=window.EventTarget,rt=["addEventListener","removeEventListener","dispatchEvent"];[Element,Window,Document].forEach(function(e){var t=e.prototype;rt.forEach(function(e){Object.defineProperty(t,e+"_",{value:t[e]})})}),O.prototype={addEventListener:function(e,t,n){if(P(t)){var r=new y(e,t,n),i=R.get(this);if(i){for(var o=0;i.length>o;o++)if(r.equals(i[o]))return}else i=[],R.set(this,i);i.push(r);var a=C(this);a.addEventListener_(e,p,!0)}},removeEventListener:function(e,t,n){n=Boolean(n);var r=R.get(this);if(r){for(var i=0,o=!1,a=0;r.length>a;a++)r[a].type===e&&r[a].capture===n&&(i++,r[a].handler===t&&(o=!0,r[a].remove()));if(o&&1===i){var s=C(this);s.removeEventListener_(e,p,!0)}}},dispatchEvent:function(e){var t=C(this);return t.dispatchEvent_(x(e))}},nt&&A(nt,O);var it=document.elementFromPoint;e.adjustRelatedTarget=l,e.elementFromPoint=_,e.wrapEventTargetMethods=L,e.wrappers.CustomEvent=G,e.wrappers.Event=w,e.wrappers.EventTarget=O,e.wrappers.FocusEvent=$,e.wrappers.MouseEvent=Q,e.wrappers.MutationEvent=J,e.wrappers.UIEvent=V}(this.ShadowDOMPolyfill),function(e){"use strict";function t(e,t){Object.defineProperty(e,t,{enumerable:!1})}function n(){this.length=0,t(this,"length")}function r(e){if(null==e)return e;for(var t=new n,r=0,i=e.length;i>r;r++)t[r]=o(e[r]);return t.length=i,t}function i(e,t){e.prototype[t]=function(){return r(this.impl[t].apply(this.impl,arguments))}}var o=e.wrap;n.prototype={item:function(e){return this[e]}},t(n.prototype,"item"),e.wrappers.NodeList=n,e.addWrapNodeListMethod=i,e.wrapNodeList=r}(this.ShadowDOMPolyfill),function(e){"use strict";function t(e){u(e instanceof o)}function n(e,t,n,r){if(e.nodeType!==o.DOCUMENT_FRAGMENT_NODE)return e.parentNode&&e.parentNode.removeChild(e),e.parentNode_=t,e.previousSibling_=n,e.nextSibling_=r,n&&(n.nextSibling_=e),r&&(r.previousSibling_=e),[e];for(var i,a=[];i=e.firstChild;)e.removeChild(i),a.push(i),i.parentNode_=t;for(var s=0;a.length>s;s++)a[s].previousSibling_=a[s-1]||n,a[s].nextSibling_=a[s+1]||r;return n&&(n.nextSibling_=a[0]),r&&(r.previousSibling_=a[a.length-1]),a}function r(e){if(1===e.length)return h(e[0]);for(var t=h(document.createDocumentFragment()),n=0;e.length>n;n++)t.appendChild(h(e[n]));return t}function i(e){for(var t=e.firstChild;t;){u(t.parentNode===e);var n=t.nextSibling,r=h(t),i=r.parentNode;i&&b.call(i,r),t.previousSibling_=t.nextSibling_=t.parentNode_=null,t=n}e.firstChild_=e.lastChild_=null}function o(e){u(e instanceof f),a.call(this,e),this.parentNode_=void 0,this.firstChild_=void 0,this.lastChild_=void 0,this.nextSibling_=void 0,this.previousSibling_=void 0}var a=e.wrappers.EventTarget,s=e.wrappers.NodeList,l=e.defineWrapGetter,u=e.assert,c=e.mixin,d=e.registerWrapper,h=e.unwrap,p=e.wrap,f=window.Node,v=f.prototype.appendChild,m=f.prototype.insertBefore,g=f.prototype.replaceChild,b=f.prototype.removeChild,y=f.prototype.compareDocumentPosition;o.prototype=Object.create(a.prototype),c(o.prototype,{appendChild:function(e){t(e),this.invalidateShadowRenderer();var i=this.lastChild,o=null,a=n(e,this,i,o);return this.lastChild_=a[a.length-1],i||(this.firstChild_=a[0]),v.call(this.impl,r(a)),e},insertBefore:function(e,i){if(!i)return this.appendChild(e);t(e),t(i),u(i.parentNode===this),this.invalidateShadowRenderer();var o=i.previousSibling,a=i,s=n(e,this,o,a);this.firstChild===i&&(this.firstChild_=s[0]);var l=h(i),c=l.parentNode;return c&&m.call(c,r(s),l),e},removeChild:function(e){if(t(e),e.parentNode!==this)throw Error("NotFoundError");this.invalidateShadowRenderer();var n=this.firstChild,r=this.lastChild,i=e.nextSibling,o=e.previousSibling,a=h(e),s=a.parentNode;return s&&b.call(s,a),n===e&&(this.firstChild_=i),r===e&&(this.lastChild_=o),o&&(o.nextSibling_=i),i&&(i.previousSibling_=o),e.previousSibling_=e.nextSibling_=e.parentNode_=null,e},replaceChild:function(e,i){if(t(e),t(i),i.parentNode!==this)throw Error("NotFoundError");this.invalidateShadowRenderer();var o=i.previousSibling,a=i.nextSibling;a===e&&(a=e.nextSibling);var s=n(e,this,o,a);this.firstChild===i&&(this.firstChild_=s[0]),this.lastChild===i&&(this.lastChild_=s[s.length-1]),i.previousSibling_=null,i.nextSibling_=null,i.parentNode_=null;var l=h(i);return l.parentNode&&g.call(l.parentNode,r(s),l),i},hasChildNodes:function(){return null===this.firstChild},get parentNode(){return void 0!==this.parentNode_?this.parentNode_:p(this.impl.parentNode)},get firstChild(){return void 0!==this.firstChild_?this.firstChild_:p(this.impl.firstChild)},get lastChild(){return void 0!==this.lastChild_?this.lastChild_:p(this.impl.lastChild)},get nextSibling(){return void 0!==this.nextSibling_?this.nextSibling_:p(this.impl.nextSibling)},get previousSibling(){return void 0!==this.previousSibling_?this.previousSibling_:p(this.impl.previousSibling)},get parentElement(){for(var e=this.parentNode;e&&e.nodeType!==o.ELEMENT_NODE;)e=e.parentNode;return e},get textContent(){for(var e="",t=this.firstChild;t;t=t.nextSibling)e+=t.textContent;return e},set textContent(e){if(i(this),this.invalidateShadowRenderer(),""!==e){var t=this.impl.ownerDocument.createTextNode(e);this.appendChild(t)}},get childNodes(){for(var e=new s,t=0,n=this.firstChild;n;n=n.nextSibling)e[t++]=n;return e.length=t,e},cloneNode:function(e){if(!this.invalidateShadowRenderer())return p(this.impl.cloneNode(e));var t=p(this.impl.cloneNode(!1));if(e)for(var n=this.firstChild;n;n=n.nextSibling)t.appendChild(n.cloneNode(!0));return t},contains:function(e){if(!e)return!1;if(e===this)return!0;var t=e.parentNode;return t?this.contains(t):!1},compareDocumentPosition:function(e){return y.call(this.impl,h(e))}}),l(o,"ownerDocument"),d(f,o,document.createDocumentFragment()),delete o.prototype.querySelector,delete o.prototype.querySelectorAll,o.prototype=c(Object.create(a.prototype),o.prototype),e.wrappers.Node=o}(this.ShadowDOMPolyfill),function(e){"use strict";function t(e,n){for(var r,i=e.firstElementChild;i;){if(i.matches(n))return i;if(r=t(i,n))return r;i=i.nextElementSibling}return null}function n(e,t,r){for(var i=e.firstElementChild;i;)i.matches(t)&&(r[r.length++]=i),n(i,t,r),i=i.nextElementSibling;return r}var r={querySelector:function(e){return t(this,e)},querySelectorAll:function(e){return n(this,e,new NodeList)}},i={getElementsByTagName:function(e){return this.querySelectorAll(e)},getElementsByClassName:function(e){return this.querySelectorAll("."+e)},getElementsByTagNameNS:function(e,t){if("*"===e)return this.getElementsByTagName(t);for(var n=new NodeList,r=this.getElementsByTagName(t),i=0,o=0;r.length>i;i++)r[i].namespaceURI===e&&(n[o++]=r[i]);return n.length=o,n}};e.GetElementsByInterface=i,e.SelectorsInterface=r}(this.ShadowDOMPolyfill),function(e){"use strict";function t(e){for(;e&&e.nodeType!==Node.ELEMENT_NODE;)e=e.nextSibling;return e}function n(e){for(;e&&e.nodeType!==Node.ELEMENT_NODE;)e=e.previousSibling;return e}var r=e.wrappers.NodeList,i={get firstElementChild(){return t(this.firstChild)},get lastElementChild(){return n(this.lastChild)},get childElementCount(){for(var e=0,t=this.firstElementChild;t;t=t.nextElementSibling)e++;return e},get children(){for(var e=new r,t=0,n=this.firstElementChild;n;n=n.nextElementSibling)e[t++]=n;return e.length=t,e}},o={get nextElementSibling(){return t(this.nextSibling)},get previousElementSibling(){return n(this.nextSibling)}};e.ChildNodeInterface=o,e.ParentNodeInterface=i}(this.ShadowDOMPolyfill),function(e){"use strict";function t(e){r.call(this,e)}var n=e.ChildNodeInterface,r=e.wrappers.Node,i=e.mixin,o=e.registerWrapper,a=window.CharacterData;t.prototype=Object.create(r.prototype),i(t.prototype,{get textContent(){return this.data},set textContent(e){this.data=e}}),i(t.prototype,n),o(a,t,document.createTextNode("")),e.wrappers.CharacterData=t}(this.ShadowDOMPolyfill),function(e){"use strict";function t(e){i.call(this,e)}var n=e.ChildNodeInterface,r=e.GetElementsByInterface,i=e.wrappers.Node,o=e.ParentNodeInterface,a=e.SelectorsInterface;e.addWrapNodeListMethod;var s=e.mixin,l=e.registerWrapper,u=e.wrappers,c=new SideTable,d=window.Element,h=d.prototype.matches||d.prototype.mozMatchesSelector||d.prototype.msMatchesSelector||d.prototype.webkitMatchesSelector;t.prototype=Object.create(i.prototype),s(t.prototype,{createShadowRoot:function(){var t=new u.ShadowRoot(this);return c.set(this,t),e.getRendererForHost(this),this.invalidateShadowRenderer(!0),t},get shadowRoot(){return c.get(this)||null},setAttribute:function(e,t){this.impl.setAttribute(e,t),this.invalidateShadowRenderer()},matches:function(e){return h.call(this.impl,e)}}),s(t.prototype,n),s(t.prototype,r),s(t.prototype,o),s(t.prototype,a),l(d,t),e.wrappers.Element=t}(this.ShadowDOMPolyfill),function(e){"use strict";function t(e){switch(e){case"&":return"&";case"<":return"<";case'"':return"""}}function n(e){return e.replace(v,t)}function r(e){switch(e.nodeType){case Node.ELEMENT_NODE:for(var t,r=e.tagName.toLowerCase(),o="<"+r,a=e.attributes,s=0;t=a[s];s++)o+=" "+t.name+'="'+n(t.value)+'"';return o+=">",m[r]?o:o+i(e)+"";case Node.TEXT_NODE:return n(e.nodeValue);case Node.COMMENT_NODE:return"";default:throw console.error(e),Error("not implemented")}}function i(e){for(var t="",n=e.firstChild;n;n=n.nextSibling)t+=r(n);return t}function o(e,t,n){var r=n||"div";e.textContent="";var i=p(e.ownerDocument.createElement(r));i.innerHTML=t;for(var o;o=i.firstChild;)e.appendChild(f(o))}function a(e){u.call(this,e)}function s(t){c(a,t,function(){return e.renderAllPending(),this.impl[t]})}function l(t){Object.defineProperty(a.prototype,t,{value:function(){return e.renderAllPending(),this.impl[t].apply(this.impl,arguments)},configurable:!0,enumerable:!0})}var u=e.wrappers.Element,c=e.defineGetter,d=e.mixin,h=e.registerWrapper,p=e.unwrap,f=e.wrap,v=/&|<|"/g,m={area:!0,base:!0,br:!0,col:!0,command:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0},g=window.HTMLElement;a.prototype=Object.create(u.prototype),d(a.prototype,{get innerHTML(){return i(this)},set innerHTML(e){o(this,e,this.tagName)},get outerHTML(){return r(this)},set outerHTML(e){if(this.invalidateShadowRenderer())throw Error("not implemented");this.impl.outerHTML=e}}),["clientHeight","clientLeft","clientTop","clientWidth","offsetHeight","offsetLeft","offsetTop","offsetWidth","scrollHeight","scrollLeft","scrollTop","scrollWidth"].forEach(s),["getBoundingClientRect","getClientRects","scrollIntoView"].forEach(l),h(g,a,document.createElement("b")),e.wrappers.HTMLElement=a,e.getInnerHTML=i,e.setInnerHTML=o}(this.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.HTMLElement,r=e.mixin,i=e.registerWrapper,o=window.HTMLContentElement;t.prototype=Object.create(n.prototype),r(t.prototype,{get select(){return this.getAttribute("select")},set select(e){this.setAttribute("select",e)},setAttribute:function(e,t){n.prototype.setAttribute.call(this,e,t),"select"===(e+"").toLowerCase()&&this.invalidateShadowRenderer(!0)}}),o&&i(o,t),e.wrappers.HTMLContentElement=t}(this.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e),this.olderShadowRoot_=null}var n=e.wrappers.HTMLElement,r=e.mixin,i=e.registerWrapper,o=window.HTMLShadowElement;t.prototype=Object.create(n.prototype),r(t.prototype,{get olderShadowRoot(){return this.olderShadowRoot_},invalidateShadowRenderer:function(){n.prototype.invalidateShadowRenderer.call(this,!0)}}),o&&i(o,t),e.wrappers.HTMLShadowElement=t}(this.ShadowDOMPolyfill),function(e){"use strict";function t(e){if(!e.defaultView)return e;var t=d.get(e);if(!t){for(t=e.implementation.createHTMLDocument("");t.lastChild;)t.removeChild(t.lastChild);d.set(e,t)}return t}function n(e){for(var n,r=t(e.ownerDocument),i=r.createDocumentFragment();n=e.firstChild;)i.appendChild(n);return i}function r(e){i.call(this,e)}var i=e.wrappers.HTMLElement,o=e.getInnerHTML,a=e.mixin,s=e.registerWrapper,l=e.setInnerHTML,u=e.wrap,c=new SideTable,d=new SideTable,h=window.HTMLTemplateElement;r.prototype=Object.create(i.prototype),a(r.prototype,{get content(){if(h)return u(this.impl.content);var e=c.get(this);return e||(e=n(this),c.set(this,e)),e},get innerHTML(){return o(this.content)},set innerHTML(e){l(this.content,e),this.invalidateShadowRenderer()}}),h&&s(h,r),e.wrappers.HTMLTemplateElement=r}(this.ShadowDOMPolyfill),function(e){"use strict";function t(e){switch(e.localName){case"content":return new n(e);case"shadow":return new i(e);case"template":return new o(e)}r.call(this,e)}var n=e.wrappers.HTMLContentElement,r=e.wrappers.HTMLElement,i=e.wrappers.HTMLShadowElement,o=e.wrappers.HTMLTemplateElement;e.mixin;var a=e.registerWrapper,s=window.HTMLUnknownElement;t.prototype=Object.create(r.prototype),a(s,t),e.wrappers.HTMLUnknownElement=t}(this.ShadowDOMPolyfill),function(e){"use strict";var t=e.GetElementsByInterface,n=e.ParentNodeInterface,r=e.SelectorsInterface,i=e.mixin,o=e.registerObject,a=o(document.createDocumentFragment());i(a.prototype,n),i(a.prototype,r),i(a.prototype,t);var s=o(document.createTextNode("")),l=o(document.createComment(""));e.wrappers.Comment=l,e.wrappers.DocumentFragment=a,e.wrappers.Text=s}(this.ShadowDOMPolyfill),function(e){"use strict";function t(t){var r=l(t.impl.ownerDocument.createDocumentFragment());n.call(this,r),a(r,this);var i=t.shadowRoot;e.nextOlderShadowTreeTable.set(this,i),u.set(this,t)}var n=e.wrappers.DocumentFragment,r=e.elementFromPoint,i=e.getInnerHTML,o=e.mixin,a=e.rewrap,s=e.setInnerHTML,l=e.unwrap,u=new SideTable;t.prototype=Object.create(n.prototype),o(t.prototype,{get innerHTML(){return i(this)},set innerHTML(e){s(this,e),this.invalidateShadowRenderer()},invalidateShadowRenderer:function(){return u.get(this).invalidateShadowRenderer()},elementFromPoint:function(e,t){return r(this,this.ownerDocument,e,t)},getElementById:function(e){return this.querySelector("#"+e)}}),e.wrappers.ShadowRoot=t,e.getHostForShadowRoot=function(e){return u.get(e)}}(this.ShadowDOMPolyfill),function(e){"use strict";function t(e){e.previousSibling_=e.previousSibling,e.nextSibling_=e.nextSibling,e.parentNode_=e.parentNode}function n(e){e.firstChild_=e.firstChild,e.lastChild_=e.lastChild}function r(e){D(e instanceof N);for(var r=e.firstChild;r;r=r.nextSibling)t(r);n(e)}function i(e){var t=x(e);r(e),t.textContent=""}function o(e,n){var i=x(e),o=x(n);o.nodeType===N.DOCUMENT_FRAGMENT_NODE?r(n):(s(n),t(n)),e.lastChild_=e.lastChild,e.lastChild===e.firstChild&&(e.firstChild_=e.firstChild);var a=H(i.lastChild);a&&(a.nextSibling_=a.nextSibling),i.appendChild(o)}function a(e,n){var r=x(e),i=x(n);t(n),n.previousSibling&&(n.previousSibling.nextSibling_=n),n.nextSibling&&(n.nextSibling.previousSibling_=n),e.lastChild===n&&(e.lastChild_=n),e.firstChild===n&&(e.firstChild_=n),r.removeChild(i)}function s(e){var t=x(e),n=t.parentNode;n&&a(H(n),e)}function l(e,t){c(t).push(e),I.set(e,t);var n=R.get(e);n||R.set(e,n=[]),n.push(t)}function u(e){k.set(e,[])}function c(e){return k.get(e)}function d(e){for(var t=[],n=0,r=e.firstChild;r;r=r.nextSibling)t[n++]=r;return t}function h(e,t,n){for(var r=d(e),i=0;r.length>i;i++){var o=r[i];if(t(o)){if(n(o)===!1)return}else h(o,t,n)}}function p(e,t){var n=!1;return h(e,w,function(e){u(e);for(var r=0;t.length>r;r++){var i=t[r];void 0!==i&&v(i,e)&&(l(i,e),t[r]=void 0,n=!0)}}),n?t.filter(function(e){return void 0!==e}):t}function f(e,t){for(var n=0;t.length>n;n++)if(t[n]in e)return t[n]}function v(e,t){var n=t.getAttribute("select");if(!n)return!0;if(n=n.trim(),!n)return!0;if(e.nodeType!==N.ELEMENT_NODE)return!1;if(!B.test(n))return!1;if(":"===n[0]&&!q.test(n))return!1;try{return e.matches(n)}catch(r){return!1}}function m(){L=null,W.forEach(function(e){e.render()}),W=[]}function g(e){this.host=e,this.dirty=!1,this.associateNode(e)}function b(e){var t=F.get(e);return t||(t=new g(e),F.set(e,t)),t}function y(e){return"content"===e.localName}function w(e){return"content"===e.localName}function E(e){return"shadow"===e.localName}function T(e){return"shadow"===e.localName}function M(e){return!!e.shadowRoot}function S(e){return j.get(e)}function P(e){for(var t=[],n=e.shadowRoot;n;n=j.get(n))t.push(n);return t}function O(e,t){I.set(e,t)}function C(e){new g(e).render()}var L,_=e.wrappers.HTMLContentElement,N=e.wrappers.Node,D=e.assert,A=e.mixin,x=e.unwrap,H=e.wrap,k=new SideTable,R=new SideTable,I=new SideTable,j=new SideTable,F=new SideTable,U=new SideTable,B=/^[*.:#[a-zA-Z_|]/,q=RegExp("^:("+["link","visited","target","enabled","disabled","checked","indeterminate","nth-child","nth-last-child","nth-of-type","nth-last-of-type","first-child","last-child","first-of-type","last-of-type","only-of-type"].join("|")+")"),Y=f(window,["requestAnimationFrame","mozRequestAnimationFrame","webkitRequestAnimationFrame","setTimeout"]),W=[];g.prototype={render:function(){if(this.dirty){var e=this.host;this.treeComposition();var t=e.shadowRoot;if(t){this.removeAllChildNodes(this.host);var n=d(t);n.forEach(function(n){this.renderNode(e,t,n,!1)},this),this.dirty=!1}}},invalidate:function(){if(!this.dirty){if(this.dirty=!0,W.push(this),L)return;L=window[Y](m,0)}},renderNode:function(e,t,n,r){if(M(n)){this.appendChild(e,n);var i=b(n);i.dirty=!0,i.render()}else y(n)?this.renderInsertionPoint(e,t,n,r):E(n)?this.renderShadowInsertionPoint(e,t,n):this.renderAsAnyDomTree(e,t,n,r)},renderAsAnyDomTree:function(e,t,n,r){if(this.appendChild(e,n),M(n))C(n);else{var i=n,o=d(i);o.forEach(function(e){this.renderNode(i,t,e,r)},this)}},renderInsertionPoint:function(e,t,n,r){var i=c(n);i.length?(this.removeAllChildNodes(n),i.forEach(function(n){y(n)&&r?this.renderInsertionPoint(e,t,n,r):this.renderAsAnyDomTree(e,t,n,r)},this)):this.renderFallbackContent(e,n),this.remove(n)},renderShadowInsertionPoint:function(e,t,n){var r=S(t);if(r){I.set(r,n),n.olderShadowRoot_=r,this.remove(n);var i=d(r);i.forEach(function(t){this.renderNode(e,r,t,!0)},this)}else this.renderFallbackContent(e,n)},renderFallbackContent:function(e,t){var n=d(t);n.forEach(function(t){this.appendChild(e,t)},this)},treeComposition:function(){var e=this.host,t=e.shadowRoot,n=[],r=d(e);r.forEach(function(e){if(y(e)){var t=c(e);t&&t.length||(t=d(e)),n.push.apply(n,t)}else n.push(e)});for(var i,o;t;){if(i=void 0,h(t,T,function(e){return i=e,!1}),o=i,n=p(t,n),o){var a=S(t);if(a){t=a,O(t,o);continue}break}break}},appendChild:function(e,t){o(e,t),this.associateNode(t)},remove:function(e){s(e),this.associateNode(e)},removeAllChildNodes:function(e){i(e)},associateNode:function(e){U.set(e,this)}},N.prototype.invalidateShadowRenderer=function(e){var t=U.get(this);if(!t)return!1;var n;return(e||this.shadowRoot||(n=this.parentNode)&&(n.shadowRoot||n instanceof ShadowRoot))&&t.invalidate(),!0},_.prototype.getDistributedNodes=function(){return m(),c(this)},A(N.prototype,{get insertionParent(){return I.get(this)||null}}),e.eventParentsTable=R,e.getRendererForHost=b,e.getShadowTrees=P,e.nextOlderShadowTreeTable=j,e.renderAllPending=m,e.visual={removeAllChildNodes:i,appendChild:o,removeChild:a}}(this.ShadowDOMPolyfill),function(e){"use strict";function t(e){s.call(this,e)}function n(e){var n=document[e];t.prototype[e]=function(){return m(n.apply(this.impl,arguments))}}function r(e){this.impl=e}function i(e,t){var n=document.implementation[t];e.prototype[t]=function(){return m(n.apply(this.impl,arguments))}}function o(e,t){var n=document.implementation[t];e.prototype[t]=function(){return n.apply(this.impl,arguments)}}var a=e.GetElementsByInterface,s=e.wrappers.Node,l=e.ParentNodeInterface,u=e.SelectorsInterface,c=e.defineWrapGetter,d=e.elementFromPoint,h=e.forwardMethodsToWrapper,p=e.mixin,f=e.registerWrapper,v=e.unwrap,m=e.wrap,g=e.wrapEventTargetMethods;e.wrapNodeList;var b=new SideTable;t.prototype=Object.create(s.prototype),c(t,"documentElement"),c(t,"body"),c(t,"head"),["getElementById","createElement","createElementNS","createTextNode","createDocumentFragment","createEvent","createEventNS"].forEach(n);var y=document.adoptNode,w=document.write;p(t.prototype,{adoptNode:function(e){return y.call(this.impl,v(e)),e},elementFromPoint:function(e,t){return d(this,this,e,t)},write:function(e){for(var t=this.querySelectorAll("*"),n=t[t.length-1];n.nextSibling;)n=n.nextSibling;var r=n.parentNode;r.lastChild_=void 0,n.nextSibling_=void 0,w.call(this.impl,e)}}),h([window.HTMLBodyElement,window.HTMLDocument||window.Document,window.HTMLHeadElement],["appendChild","compareDocumentPosition","getElementsByClassName","getElementsByTagName","getElementsByTagNameNS","insertBefore","querySelector","querySelectorAll","removeChild","replaceChild"]),h([window.HTMLDocument||window.Document],["adoptNode","createDocumentFragment","createElement","createElementNS","createEvent","createEventNS","createTextNode","elementFromPoint","getElementById","write"]),p(t.prototype,a),p(t.prototype,l),p(t.prototype,u),p(t.prototype,{get implementation(){var e=b.get(this);return e?e:(e=new r(v(this).implementation),b.set(this,e),e)}}),f(window.Document,t,document.implementation.createHTMLDocument("")),window.HTMLDocument&&f(window.HTMLDocument,t),g([window.HTMLBodyElement,window.HTMLDocument||window.Document,window.HTMLHeadElement]),i(r,"createDocumentType"),i(r,"createDocument"),i(r,"createHTMLDocument"),o(r,"hasFeature"),f(window.DOMImplementation,r),h([window.DOMImplementation],["createDocumentType","createDocument","createHTMLDocument","hasFeature"]),e.wrappers.Document=t,e.wrappers.DOMImplementation=r}(this.ShadowDOMPolyfill),function(e){"use strict";function t(e){n.call(this,e)}var n=e.wrappers.EventTarget,r=e.mixin,i=e.registerWrapper,o=e.unwrap,a=e.unwrapIfNeeded,s=e.wrap,l=window.Window;t.prototype=Object.create(n.prototype);var u=window.getComputedStyle;l.prototype.getComputedStyle=function(e,t){return u.call(this||window,a(e),t)},["addEventListener","removeEventListener","dispatchEvent"].forEach(function(e){l.prototype[e]=function(){var t=s(this||window);return t[e].apply(t,arguments)}}),r(t.prototype,{getComputedStyle:function(e,t){return u.call(o(this),a(e),t)}}),i(l,t),e.wrappers.Window=t}(this.ShadowDOMPolyfill),function(e){"use strict";function t(e){this.impl=e}function n(e){return new t(e)}function r(e){return e.map(n)}function i(e){var t=this;this.impl=new c(function(n){e.call(t,r(n),t)})}var o=e.defineGetter,a=e.defineWrapGetter,s=e.registerWrapper,l=e.unwrapIfNeeded,u=e.wrapNodeList;e.wrappers;var c=window.MutationObserver||window.WebKitMutationObserver;if(c){var d=window.MutationRecord;t.prototype={get addedNodes(){return u(this.impl.addedNodes)},get removedNodes(){return u(this.impl.removedNodes) -}},["target","previousSibling","nextSibling"].forEach(function(e){a(t,e)}),["type","attributeName","attributeNamespace","oldValue"].forEach(function(e){o(t,e,function(){return this.impl[e]})}),d&&s(d,t),window.Node,i.prototype={observe:function(e,t){this.impl.observe(l(e),t)},disconnect:function(){this.impl.disconnect()},takeRecords:function(){return r(this.impl.takeRecords())}},e.wrappers.MutationObserver=i,e.wrappers.MutationRecord=t}}(this.ShadowDOMPolyfill),function(e){"use strict";function t(e){var t=n[e],r=window[t];if(r){var i=document.createElement(e),o=i.constructor;window[t]=o}}e.isWrapperFor;var n={a:"HTMLAnchorElement",applet:"HTMLAppletElement",area:"HTMLAreaElement",audio:"HTMLAudioElement",br:"HTMLBRElement",base:"HTMLBaseElement",body:"HTMLBodyElement",button:"HTMLButtonElement",canvas:"HTMLCanvasElement",dl:"HTMLDListElement",datalist:"HTMLDataListElement",dir:"HTMLDirectoryElement",div:"HTMLDivElement",embed:"HTMLEmbedElement",fieldset:"HTMLFieldSetElement",font:"HTMLFontElement",form:"HTMLFormElement",frame:"HTMLFrameElement",frameset:"HTMLFrameSetElement",hr:"HTMLHRElement",head:"HTMLHeadElement",h1:"HTMLHeadingElement",html:"HTMLHtmlElement",iframe:"HTMLIFrameElement",input:"HTMLInputElement",li:"HTMLLIElement",label:"HTMLLabelElement",legend:"HTMLLegendElement",link:"HTMLLinkElement",map:"HTMLMapElement",menu:"HTMLMenuElement",menuitem:"HTMLMenuItemElement",meta:"HTMLMetaElement",meter:"HTMLMeterElement",del:"HTMLModElement",ol:"HTMLOListElement",object:"HTMLObjectElement",optgroup:"HTMLOptGroupElement",option:"HTMLOptionElement",output:"HTMLOutputElement",p:"HTMLParagraphElement",param:"HTMLParamElement",pre:"HTMLPreElement",progress:"HTMLProgressElement",q:"HTMLQuoteElement",script:"HTMLScriptElement",select:"HTMLSelectElement",source:"HTMLSourceElement",span:"HTMLSpanElement",style:"HTMLStyleElement",caption:"HTMLTableCaptionElement",col:"HTMLTableColElement",table:"HTMLTableElement",tr:"HTMLTableRowElement",thead:"HTMLTableSectionElement",tbody:"HTMLTableSectionElement",textarea:"HTMLTextAreaElement",title:"HTMLTitleElement",ul:"HTMLUListElement",video:"HTMLVideoElement"};Object.keys(n).forEach(t),Object.getOwnPropertyNames(e.wrappers).forEach(function(t){window[t]=e.wrappers[t]}),e.knownElements=n}(this.ShadowDOMPolyfill),function(){window.wrap=function(e){return e.impl?e:ShadowDOMPolyfill.wrap(e)},window.unwrap=function(e){return e.impl?ShadowDOMPolyfill.unwrap(e):e};var e=window.getComputedStyle;window.getComputedStyle=function(t,n){return e.call(window,wrap(t),n)},Object.defineProperties(HTMLElement.prototype,{webkitShadowRoot:{get:function(){return this.shadowRoot}}}),HTMLElement.prototype.webkitCreateShadowRoot=HTMLElement.prototype.createShadowRoot}()}else{var SideTable;"undefined"!=typeof WeakMap&&0>navigator.userAgent.indexOf("Firefox/")?SideTable=WeakMap:function(){var e=Object.defineProperty,t=Object.hasOwnProperty,n=(new Date).getTime()%1e9;SideTable=function(){this.name="__st"+(1e9*Math.random()>>>0)+(n++ +"__")},SideTable.prototype={set:function(t,n){e(t,this.name,{value:n,writable:!0})},get:function(e){return t.call(e,this.name)?e[this.name]:void 0},"delete":function(e){this.set(e,void 0)}}}(),function(){window.templateContent=window.templateContent||function(e){return e.content},window.wrap=window.unwrap=function(e){return e},window.createShadowRoot=function(e){return e.webkitCreateShadowRoot()},window.templateContent=function(e){if(window.HTMLTemplateElement&&HTMLTemplateElement.bootstrap&&HTMLTemplateElement.bootstrap(e),!e.content&&!e._content){for(var t=document.createDocumentFragment();e.firstChild;)t.appendChild(e.firstChild);e._content=t}return e.content||e._content}}()}if(function(e){Function.prototype.bind||(Function.prototype.bind=function(e){var t=this,n=Array.prototype.slice.call(arguments,1);return function(){var r=n.slice();return r.push.apply(r,arguments),t.apply(e,r)}}),e.mixin=window.mixin}(window.Platform),function(e){"use strict";function t(e,t,n){var r="string"==typeof e?document.createElement(e):e.cloneNode(!0);if(r.innerHTML=t,n)for(var i in n)r.setAttribute(i,n[i]);return r}var n=DOMTokenList.prototype.add,r=DOMTokenList.prototype.remove;if(DOMTokenList.prototype.add=function(){for(var e=0;arguments.length>e;e++)n.call(this,arguments[e])},DOMTokenList.prototype.remove=function(){for(var e=0;arguments.length>e;e++)r.call(this,arguments[e])},DOMTokenList.prototype.toggle=function(e,t){1==arguments.length&&(t=!this.contains(e)),t?this.add(e):this.remove(e)},DOMTokenList.prototype.switch=function(e,t){e&&this.remove(e),t&&this.add(t)},NodeList.prototype.forEach=function(e,t){Array.prototype.slice.call(this).forEach(e,t)},HTMLCollection.prototype.forEach=function(e,t){Array.prototype.slice.call(this).forEach(e,t)},!window.performance){var i=Date.now();window.performance={now:function(){return Date.now()-i}}}window.requestAnimationFrame||(window.requestAnimationFrame=function(){var e=window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame;return e?function(t){return e(function(){t(performance.now())})}:function(e){return window.setTimeout(e,1e3/60)}}()),window.cancelAnimationFrame||(window.cancelAnimationFrame=function(){return window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||function(e){clearTimeout(e)}}()),e.createDOM=t}(window.Platform),window.templateContent=window.templateContent||function(e){return e.content},function(e){e=e||(window.Inspector={});var t;window.sinspect=function(e,r){t||(t=window.open("","ShadowDOM Inspector",null,!0),t.document.write(n),t.api={shadowize:shadowize}),o(e||wrap(document.body),r)};var n=["",""," "," ShadowDOM Inspector"," "," "," ",'
      ',"
    ",'
    '," ",""].join("\n"),r=[],i=function(){var e=t.document,n=e.querySelector("#crumbs");n.textContent="";for(var i,a=0;i=r[a];a++){var s=e.createElement("a");s.href="#",s.textContent=i.localName,s.idx=a,s.onclick=function(e){for(var t;r.length>this.idx;)t=r.pop();o(t.shadow||t,t),e.preventDefault()},n.appendChild(e.createElement("li")).appendChild(s)}},o=function(e,n){var o=t.document;c=[];var a=n||e;r.push(a),i(),o.body.querySelector("#tree").innerHTML="
    "+u(e,e.childNodes)+"
    "},a=Array.prototype.forEach.call.bind(Array.prototype.forEach),s={STYLE:1,SCRIPT:1,"#comment":1,TEMPLATE:1},l=function(e){return s[e.nodeName]},u=function(e,t,n){if(l(e))return"";var r=n||"";if(e.localName||11==e.nodeType){var i=e.localName||"shadow-root",o=r+d(e);"content"==i&&(t=e.getDistributedNodes()),o+="
    ";var s=r+"  ";a(t,function(e){o+=u(e,e.childNodes,s)}),o+=r,{br:1}[i]||(o+="</"+i+">",o+="
    ")}else{var c=e.textContent.trim();o=c?r+'"'+c+'"'+"
    ":""}return o},c=[],d=function(e){var t="<",n=e.localName||"shadow-root";return e.webkitShadowRoot||e.shadowRoot?(t+=' ",c.push(e)):t+=n||"shadow-root",e.attributes&&a(e.attributes,function(e){t+=" "+e.name+(e.value?'="'+e.value+'"':"")}),t+=">"};shadowize=function(){var e=Number(this.attributes.idx.value),t=c[e];t?o(t.webkitShadowRoot||t.shadowRoot,t):(console.log("bad shadowize node"),console.dir(this))},e.output=u}(window.Inspector),function(e){"use strict";function t(e){return+e===e>>>0}function n(e){return+e}function r(e){return e===Object(e)}function i(e,t){return e===t?0!==e||1/e===1/t:R(e)&&R(t)?!0:e!==e&&t!==t}function o(e){return"string"!=typeof e?!1:(e=e.replace(/\s/g,""),""==e?!0:"."==e[0]?!1:U.test(e))}function a(e){return""==e.trim()?this:t(e)?(this.push(e+""),this):(e.split(/\./).filter(function(e){return e}).forEach(function(e){this.push(e)},this),void 0)}function s(e){for(var t=0;B>t&&e.check();)e.report(),t++}function l(e){for(var t in e)return!1;return!0}function u(e){return l(e.added)&&l(e.removed)&&l(e.changed)}function c(e,t){var n={},r={},i={};for(var o in t){var a=e[o];(void 0===a||a!==t[o])&&(o in e?a!==t[o]&&(i[o]=a):r[o]=void 0)}for(var o in e)o in t||(n[o]=e[o]);return Array.isArray(e)&&e.length!==t.length&&(i.length=e.length),{added:n,removed:r,changed:i}}function d(e,t){var n=t||(Array.isArray(e)?[]:{});for(var r in e)n[r]=e[r];return Array.isArray(e)&&(n.length=e.length),n}function h(e){this.callback=e,this.reporting=!0,A&&(this.boundInternalCallback=this.internalCallback.bind(this)),this.valid=!0,p(this),this.connect(),this.sync(!0)}function p(e){Y&&q.push(e)}function f(e){if(Y)for(var t=0;q.length>t;t++)if(q[t]===e){q[t]=void 0;break}}function v(e,t){this.object=e,h.call(this,t)}function m(e,t){if(!Array.isArray(e))throw Error("Provided object is not an Array");this.object=e,h.call(this,t)}function g(e,t){var n;return t.walkPropertiesFrom(e,function(e,r,i){i===t.length&&(n=r)}),n}function b(e,t,n){var i=!1;return t.walkPropertiesFrom(e,function(e,o,a){r(o)&&a==t.length-1&&(i=!0,o[e]=n)}),i}function y(e){var t="",n="obj",r=e.length;t+="if (obj";for(var i=0;r-1>i;i++){var o='["'+e[i]+'"]';n+=o,t+=" && "+n}return t+=") ",n+='["'+e[r-1]+'"]',t+="return "+n+"; else return undefined;",Function("obj",t)}function w(e,t){var n=""+t;return V[n]||(V[n]=y(t)),V[n](e)}function E(t,n,i,o,a){var s=void 0;return n.walkPropertiesFrom(t,function(t,l,u){if(u===n.length)return s=l,void 0;var c=i[u];if(!c||l!==c[0]){if(c)for(var d=0;c.length>d;d++){var h=c[d],p=o.get(h);1==p?(o.delete(h),e.unobserveCount++,Object.unobserve(h,a)):o.set(h,p-1)}if(c=l,r(c)){for(var c=[];r(l);){c.push(l);var p=o.get(l);p?o.set(l,p+1):(o.set(l,1),e.observeCount++,Object.observe(l,a)),l=Object.getPrototypeOf(l)}i[u]=c}}},this),s}function T(e,t,n){if(this.value=void 0,o(t)){var i=new a(t);return i.length?(r(e)&&(this.object=e,this.path=i,A?(this.observed=Array(i.length),this.observedMap=new Map,this.getPathValue=E):this.getPathValue=x?y(this.path):g,h.call(this,n)),void 0):(this.value=e,void 0)}}function M(e,t){if("function"==typeof Object.observe){var n=Object.getNotifier(e);return function(r,i){var o={object:e,type:r,name:t};2===arguments.length&&(o.oldValue=i),n.notify(o)}}}function S(e,t,n){for(var r={},i={},o=0;t.length>o;o++){var a=t[o];G[a.type]?(a.name in n||(n[a.name]=a.oldValue),"updated"!=a.type&&("new"!=a.type?a.name in r?(delete r[a.name],delete n[a.name]):i[a.name]=!0:a.name in i?delete i[a.name]:r[a.name]=!0)):(console.error("Unknown changeRecord type: "+a.type),console.error(a))}for(var s in r)r[s]=e[s];for(var s in i)i[s]=void 0;var l={};for(var s in n)if(!(s in r||s in i)){var u=e[s];n[s]!==u&&(l[s]=u)}return{added:r,removed:i,changed:l}}function P(e,t,n,r,i,o){for(var a=o-i+1,s=n-t+1,l=Array(a),u=0;a>u;u++)l[u]=Array(s),l[u][0]=u;for(var c=0;s>c;c++)l[0][c]=c;for(var u=1;a>u;u++)for(var c=1;s>c;c++)if(r[i+u-1]===e[t+c-1])l[u][c]=l[u-1][c-1];else{var d=l[u-1][c]+1,h=l[u][c-1]+1;l[u][c]=h>d?d:h}return l}function O(e){for(var t=e.length-1,n=e[0].length-1,r=e[t][n],i=[];t>0||n>0;)if(0!=t)if(0!=n){var o,a=e[t-1][n-1],s=e[t-1][n],l=e[t][n-1];o=l>s?a>s?s:a:a>l?l:a,o==a?(a==r?i.push(X):(i.push(z),r=a),t--,n--):o==s?(i.push(Q),t--,r=s):(i.push(K),n--,r=l)}else i.push(Q),t--;else i.push(K),n--;return i.reverse(),i}function C(e,t,n){for(var r=0;n>r;r++)if(e[r]!==t[r])return r;return n}function L(e,t,n){for(var r=e.length,i=t.length,o=0;n>o&&e[--r]===t[--i];)o++;return o}function _(e,t,n,r,i,o){function a(e,t,n){return{index:e,removed:t,addedCount:n}}var s=0,l=0,u=Math.min(n-t,o-i);if(0==t&&0==i&&(s=C(e,r,u)),n==e.length&&o==r.length&&(l=L(e,r,u-s)),t+=s,i+=s,n-=l,o-=l,0==n-t&&0==o-i)return[];if(t==n){for(var c=a(t,[],0);o>i;)c.removed.push(r[i++]);return[c]}if(i==o)return[a(t,[],n-t)];for(var d=O(P(e,t,n,r,i,o)),c=void 0,h=[],p=t,f=i,v=0;d.length>v;v++)switch(d[v]){case X:c&&(h.push(c),c=void 0),p++,f++;break;case z:c||(c=a(p,[],0)),c.addedCount++,p++,c.removed.push(r[f]),f++;break;case K:c||(c=a(p,[],0)),c.addedCount++,p++;break;case Q:c||(c=a(p,[],0)),c.removed.push(r[f]),f++}return c&&h.push(c),h}function N(e,t,r){function i(t,r){Object.keys(t).forEach(function(t){var i=n(t);if(!(I(i)||0>i||i>=a)){var l=r[i];e.length>i?s[i]=l:o.removed[i-e.length]=r[i]}})}var o,a="length"in r?n(r.length):e.length;e.length>a?o={index:a,removed:[],addedCount:e.length-a}:a>e.length&&(o={index:e.length,removed:Array(a-e.length),addedCount:0});var s=[];i(t.added,r),i(t.removed,r),i(t.changed,r);var l,u=[];for(var c in s){if(c=n(c),l){if(l.index+l.removed.length==c){l.removed.push(s[c]);continue}l.addedCount=Math.min(e.length,l.index+l.removed.length)-l.index,u.push(l),l=void 0}l={index:c,removed:[s[c]]}}return l?(l.addedCount=Math.min(e.length,l.index+l.removed.length)-l.index,o?l.index+l.removed.length==o.index?(l.addedCount=l.addedCount+o.addedCount,l.removed=l.removed.concat(o.removed),u.push(l)):(u.push(l),u.push(o)):u.push(l)):o&&u.push(o),u}function D(e,t,n){var r=[];return N(e,t,n).forEach(function(t){r=r.concat(_(e,t.index,t.index+t.addedCount,t.removed,0,t.removed.length))}),r}var A="function"==typeof Object.observe,x=!1;try{var H=Function("","return true;");x=H()}catch(k){}var R=e.Number.isNaN||function I(t){return"number"==typeof t&&e.isNaN(t)},j="__proto__"in{}?function(e){return e}:function(e){var t=e.__proto__;if(!t)return e;var n=Object.create(t);return Object.getOwnPropertyNames(e).forEach(function(t){Object.defineProperty(n,t,Object.getOwnPropertyDescriptor(e,t))}),n},F="[$a-z0-9_]+[$a-z0-9_\\d]*",U=RegExp("^(?:#?"+F+")?"+"(?:"+"(?:\\."+F+")"+")*"+"$","i");a.prototype=j({__proto__:[],toString:function(){return this.join(".")},walkPropertiesFrom:function(e,t,n){for(var r,i=0;this.length+1>i;i++)r=this[i],t.call(n,r,e,i),e=i==this.length||null===e||void 0===e?void 0:e[r]}});var B=1e3;h.prototype={valid:!1,internalCallback:function(e){this.valid&&this.reporting&&this.check(e)&&(this.report(),this.testingResults&&(this.testingResults.anyChanged=!0))},close:function(){this.valid&&(this.disconnect(),this.valid=!1,f(this))},deliver:function(e){this.valid&&(A?(this.testingResults=e,Object.deliverChangeRecords(this.boundInternalCallback),this.testingResults=void 0):s(this))},report:function(){if(this.reporting){this.sync(!1);try{this.callback.apply(void 0,this.reportArgs)}catch(e){h._errorThrownDuringCallback=!0,console.error("Exception caught during observer callback: "+e)}this.reportArgs=void 0}},reset:function(){this.valid&&(A&&(this.reporting=!1,Object.deliverChangeRecords(this.boundInternalCallback),this.reporting=!0),this.sync(!0))}};var q,Y=!A||e.forceCollectObservers;Y&&(q=[]);var W=!1;e.Platform=e.Platform||{},e.Platform.performMicrotaskCheckpoint=function(){if(Y&&!W){W=!0;var e=0,t={};do{e++;var n=q;q=[],t.anyChanged=!1;for(var r=0;n.length>r;r++){var i=n[r];i&&i.valid&&(A?i.deliver(t):i.check()&&(t.anyChanged=!0,i.report()),q.push(i))}}while(B>e&&t.anyChanged);W=!1}},Y&&(e.Platform.clearObservers=function(){q=[]}),v.prototype=j({__proto__:h.prototype,connect:function(){A&&Object.observe(this.object,this.boundInternalCallback)},sync:function(){A||(this.oldObject=d(this.object))},check:function(e){var t,n;if(A){if(!e)return!1;n={},t=S(this.object,e,n)}else n=this.oldObject,t=c(this.object,this.oldObject);return u(t)?!1:(this.reportArgs=[t.added||{},t.removed||{},t.changed||{}],this.reportArgs.push(function(e){return n[e]}),!0)},disconnect:function(){A?this.object&&Object.unobserve(this.object,this.boundInternalCallback):this.oldObject=void 0,this.object=void 0}}),m.prototype=j({__proto__:v.prototype,sync:function(){A||(this.oldObject=this.object.slice())},check:function(e){var t;if(A){if(!e)return!1;var n={},r=S(this.object,e,n);t=D(this.object,r,n)}else t=_(this.object,0,this.object.length,this.oldObject,0,this.oldObject.length);return t&&t.length?(this.reportArgs=[t],!0):!1}}),m.applySplices=function(e,t,n){n.forEach(function(n){for(var r=[n.index,n.removed.length],i=n.index;n.index+n.addedCount>i;)r.push(t[i]),i++;Array.prototype.splice.apply(e,r)})};var V={};T.prototype=j({__proto__:h.prototype,connect:function(){},disconnect:function(){this.object=void 0,this.value=void 0,this.sync(!0)},check:function(){return this.value=this.getPathValue(this.object,this.path,this.observed,this.observedMap,this.boundInternalCallback),i(this.value,this.oldValue)?!1:(this.reportArgs=[this.value,this.oldValue],!0)},sync:function(e){e&&(this.value=this.getPathValue(this.object,this.path,this.observed,this.observedMap,this.boundInternalCallback)),this.oldValue=this.value}}),T.getValueAtPath=function(e,t){if(!o(t))return void 0;var n=new a(t);return n.length?r(e)?x?w(e,n):g(e,n):void 0:e},T.setValueAtPath=function(e,t,n){if(o(t)){var i=new a(t);i.length&&r(e)&&b(e,i,n)}};var G={"new":!0,updated:!0,deleted:!0};T.defineProperty=function(e,t,n){var r=M(e,t),i=new T(n.object,n.path,function(e,t){r&&r("updated",t)});return Object.defineProperty(e,t,{get:function(){return i.deliver(),i.value},set:function(e){T.setValueAtPath(n.object,n.path,e),i.deliver()},configurable:!0}),{close:function(){r&&i.deliver(),i.close(),delete e[t]}}};var X=0,z=1,K=2,Q=3;e.Observer=h,e.ArrayObserver=m,e.ObjectObserver=v,e.PathObserver=T}(this),function(e){"use strict";function t(e){if(!e)throw Error("Assertion failed")}function n(e){return e.ownerDocument.contains(e)}function r(e,t,n){console.error("Unhandled binding to Node: ",this,e,t,n)}function i(){}function o(){}function a(e,t,n){this.model=e,this.path=t,this.changed=n,this.observer=new PathObserver(this.model,this.path,this.changed),this.changed(this.observer.value)}function s(e){return function(t){e.data=void 0==t?"":t+""}}function l(e,t,n){if("textContent"!==e)return Node.prototype.bind.call(this,e,t,n);this.unbind("textContent");var r=new a(t,n,s(this));J.set(this,r)}function u(e){if("textContent"!=e)return Node.prototype.unbind.call(this,e);var t=J.get(this);t&&(t.dispose(),J.delete(this))}function c(){this.unbind("textContent"),Node.prototype.unbindAll.call(this)}function d(e,t,n){return n?function(n){n?e.setAttribute(t,""):e.removeAttribute(t)}:function(n){e.setAttribute(t,(void 0===n?"":n)+"")}}function h(){this.bindingMap=Object.create(null)}function p(e,t,n){var r=Z.get(this);r||(r=new h,Z.set(this,r)),r.add(this,e,t,n)}function f(e){var t=Z.get(this);t&&t.remove(e)}function v(){var e=Z.get(this);e&&(Z.delete(this),e.removeAll(),Node.prototype.unbindAll.call(this))}function m(e){switch(e.type){case"checkbox":return et;case"radio":case"select-multiple":case"select-one":return"change";default:return"input"}}function g(e,t,n,r){this.element=e,this.valueProperty=t,this.boundValueChanged=this.valueChanged.bind(this),this.boundUpdateBinding=this.updateBinding.bind(this),this.binding=new a(n,r,this.boundValueChanged),this.element.addEventListener(m(this.element),this.boundUpdateBinding,!0)}function b(e,t,n){g.call(this,e,"value",t,n)}function y(e){if(!n(e))return[];if(e.form)return K(e.form.elements,function(t){return t!=e&&"INPUT"==t.tagName&&"radio"==t.type&&t.name==e.name});var t=e.ownerDocument.querySelectorAll('input[type="radio"][name="'+e.name+'"]');return K(t,function(t){return t!=e&&!t.form})}function w(e,t,n){g.call(this,e,"checked",t,n)}function E(e,t,n){switch(e){case"value":this.unbind("value"),this.removeAttribute("value"),tt.set(this,new b(this,t,n));break;case"checked":this.unbind("checked"),this.removeAttribute("checked"),nt.set(this,new w(this,t,n));break;default:return Element.prototype.bind.call(this,e,t,n)}}function T(e){switch(e){case"value":var t=tt.get(this);t&&(t.unbind(),tt.delete(this));break;case"checked":var n=nt.get(this);n&&(n.unbind(),nt.delete(this));break;default:return Element.prototype.unbind.call(this,e)}}function M(){this.unbind("value"),this.unbind("checked"),Element.prototype.unbindAll.call(this)}function S(e){return ct[e.tagName]&&e.hasAttribute("template")}function P(e){return"TEMPLATE"==e.tagName||S(e)}function O(e){return dt&&"TEMPLATE"==e.tagName}function C(e,t){var n=e.querySelectorAll(ht);P(e)&&t(e),z(n,t)}function L(e){function t(e){HTMLTemplateElement.decorate(e)||L(e.content)}C(e,t)}function _(e,t){Object.getOwnPropertyNames(t).forEach(function(n){Object.defineProperty(e,n,Object.getOwnPropertyDescriptor(t,n))})}function N(e){if(!e.defaultView)return e;var t=mt.get(e);if(!t){for(t=e.implementation.createHTMLDocument("");t.lastChild;)t.removeChild(t.lastChild);mt.set(e,t)}return t}function D(e){var t=e.ownerDocument.createElement("template");e.parentNode.insertBefore(t,e);for(var n=e.attributes,r=n.length;r-->0;){var i=n[r];ut[i.name]&&("template"!==i.name&&t.setAttribute(i.name,i.value),e.removeAttribute(i.name))}return t}function A(e,t,n){var r=e.content;if(n)return r.appendChild(t),void 0;for(var i;i=t.firstChild;)r.appendChild(i)}function x(e){"TEMPLATE"===e.tagName?dt||(ft?e.__proto__=HTMLTemplateElement.prototype:_(e,HTMLTemplateElement.prototype)):(_(e,HTMLTemplateElement.prototype),Object.defineProperty(e,"content",yt))}function H(e){var t=e.ref;return t?t.content:e.content}function k(e,t){this.type=e,this.value=t}function R(e){for(var t=[],n=e.length,r=0,i=0;n>i;){if(r=e.indexOf("{{",i),0>r){t.push(new k(Et,e.slice(i)));break}if(r>0&&r>i&&t.push(new k(Et,e.slice(i,r))),i=r+2,r=e.indexOf("}}",i),0>r){var o=e.slice(i-2),a=t[t.length-1];a&&a.type==Et?a.value+=o:t.push(new k(Et,o));break}var s=e.slice(i,r).trim();t.push(new k(Tt,s)),i=r+2}return t}function I(e,t,n,r,i){var o,a=i&&i[st];a&&"function"==typeof a&&(o=a(n,r,t,e),o&&(n=o,r="value")),e.bind(t,n,r)}function j(e,t,n,r,i){var o=R(n);if(o.length&&(1!=o.length||o[0].type!=Et)){if(1==o.length&&o[0].type==Tt)return I(e,t,r,o[0].value,i),void 0;for(var a=new V,s=0;o.length>s;s++){var l=o[s];l.type==Tt&&I(a,s,r,l.value,i)}a.combinator=function(e){for(var t="",n=0;o.length>n;n++){var r=o[n];if(r.type===Et)t+=r.value;else{var i=e[n];void 0!==i&&(t+=i)}}return t},e.bind(t,a,"value")}}function F(e,n,r){t(e);for(var i={},o=0;e.attributes.length>o;o++){var a=e.attributes[o];i[a.name]=a.value}P(e)&&(""===i[rt]&&(i[rt]="{{}}"),""===i[it]&&(i[it]="{{}}")),Object.keys(i).forEach(function(t){j(e,t,i[t],n,r)})}function U(e,n,r){t(e),e.nodeType===Node.ELEMENT_NODE?F(e,n,r):e.nodeType===Node.TEXT_NODE&&j(e,"textContent",e.data,n,r);for(var i=e.firstChild;i;i=i.nextSibling)U(i,n,r)}function B(e){if(Mt.delete(e),P(e)){var t=St.get(e);t&&(t.abandon(),St.delete(e))}e.unbindAll();for(var n=e.firstChild;n;n=n.nextSibling)B(n)}function q(e,t){var n=e.cloneNode(!1);P(n)&&(HTMLTemplateElement.decorate(n,e),t&&!n.hasAttribute(at)&&n.setAttribute(at,t));for(var r=e.firstChild;r;r=r.nextSibling)n.appendChild(q(r,t));return n}function Y(e,t,n){this.firstNode=e,this.lastNode=t,this.model=n}function W(e,t){if(e.firstChild)for(var n=new Y(e.firstChild,e.lastChild,t),r=n.firstNode;r;)Mt.set(r,n),r=r.nextSibling}function V(e){this.bindings={},this.values={},this.value=void 0,this.size=0,this.combinator_=e,this.boundResolve=this.resolve.bind(this),this.disposed=!1}function G(e){this.templateElement_=e,this.terminators=[],this.iteratedValue=void 0,this.arrayObserver=void 0,this.boundHandleSplices=this.handleSplices.bind(this),this.inputs=new V(this.resolveInputs.bind(this)),this.valueBinding=new a(this.inputs,"value",this.valueChanged.bind(this))}var X,z=Array.prototype.forEach.call.bind(Array.prototype.forEach),K=Array.prototype.filter.call.bind(Array.prototype.filter);e.Map&&"function"==typeof e.Map.prototype.forEach?X=e.Map:(X=function(){this.keys=[],this.values=[]},X.prototype={set:function(e,t){var n=this.keys.indexOf(e);0>n?(this.keys.push(e),this.values.push(t)):this.values[n]=t},get:function(e){var t=this.keys.indexOf(e);return 0>t?void 0:this.values[t]},"delete":function(e){var t=this.keys.indexOf(e);return 0>t?!1:(this.keys.splice(t,1),this.values.splice(t,1),!0)},forEach:function(e,t){for(var n=0;this.keys.length>n;n++)e.call(t||this,this.values[n],this.keys[n],this)}});var Q="__proto__"in{}?function(e){return e}:function(e){var t=e.__proto__;if(!t)return e;var n=Object.create(t);return Object.getOwnPropertyNames(e).forEach(function(t){Object.defineProperty(n,t,Object.getOwnPropertyDescriptor(e,t))}),n};"function"!=typeof document.contains&&(Document.prototype.contains=function(e){return e===this||e.parentNode===this?!0:this.documentElement.contains(e)});var $;"undefined"!=typeof WeakMap&&0>navigator.userAgent.indexOf("Firefox/")?$=WeakMap:function(){var e=Object.defineProperty,t=Object.hasOwnProperty,n=(new Date).getTime()%1e9;$=function(){this.name="__st"+(1e9*Math.random()>>>0)+(n++ +"__")},$.prototype={set:function(t,n){e(t,this.name,{value:n,writable:!0})},get:function(e){return t.call(e,this.name)?e[this.name]:void 0},"delete":function(e){this.set(e,void 0)}}}(),Node.prototype.bind=r,Node.prototype.unbind=i,Node.prototype.unbindAll=o;var J=new $("textContentBinding");a.prototype={dispose:function(){this.observer.close()},set value(e){PathObserver.setValueAtPath(this.model,this.path,e)},reset:function(){this.observer.reset()}},Text.prototype.bind=l,Text.prototype.unbind=u,Text.prototype.unbindAll=c;var Z=new $("attributeBindings");h.prototype={add:function(e,t,n,r){e.removeAttribute(t);var i="?"==t[t.length-1];i&&(t=t.slice(0,-1)),this.remove(t);var o=new a(n,r,d(e,t,i));this.bindingMap[t]=o},remove:function(e){var t=this.bindingMap[e];t&&(t.dispose(),delete this.bindingMap[e])},removeAll:function(){Object.keys(this.bindingMap).forEach(function(e){this.remove(e)},this)}},Element.prototype.bind=p,Element.prototype.unbind=f,Element.prototype.unbindAll=v;var et,tt=new $("valueBinding"),nt=new $("checkedBinding");(function(){var e=document.createElement("div"),t=e.appendChild(document.createElement("input"));t.setAttribute("type","checkbox");var n,r=0;t.addEventListener("click",function(){r++,n=n||"click"}),t.addEventListener("change",function(){r++,n=n||"change"});var i=document.createEvent("MouseEvent");i.initMouseEvent("click",!0,!0,window,0,0,0,0,0,!1,!1,!1,!1,0,null),t.dispatchEvent(i),et=1==r?"change":n})(),g.prototype={valueChanged:function(e){this.element[this.valueProperty]=this.produceElementValue(e)},updateBinding:function(){this.binding.value=this.element[this.valueProperty],this.binding.reset(),this.postUpdateBinding&&this.postUpdateBinding(),Platform.performMicrotaskCheckpoint()},unbind:function(){this.binding.dispose(),this.element.removeEventListener(m(this.element),this.boundUpdateBinding,!0)}},b.prototype=Q({__proto__:g.prototype,produceElementValue:function(e){return(null==e?"":e)+""}}),w.prototype=Q({__proto__:g.prototype,produceElementValue:function(e){return Boolean(e)},postUpdateBinding:function(){"INPUT"===this.element.tagName&&"radio"===this.element.type&&y(this.element).forEach(function(e){var t=nt.get(e);t&&(t.binding.value=!1)})}}),HTMLInputElement.prototype.bind=E,HTMLInputElement.prototype.unbind=T,HTMLInputElement.prototype.unbindAll=M;var rt="bind",it="repeat",ot="if",at="syntax",st="getBinding",lt="getInstanceModel",ut={template:!0,repeat:!0,bind:!0,ref:!0},ct={THEAD:!0,TBODY:!0,TFOOT:!0,TH:!0,TR:!0,TD:!0,COLGROUP:!0,COL:!0,CAPTION:!0,OPTION:!0},dt="undefined"!=typeof HTMLTemplateElement,ht="template, "+Object.keys(ct).map(function(e){return e.toLowerCase()+"[template]"}).join(", "),pt=function(){function e(e){r.indexOf(e)>=0||n.indexOf(e)>=0||(n.push(e),o==i.value&&(i.value=!i.value))}function t(){for(o=i.value,r=n,n=[];r.length;){var e=r.shift();e()}}var n=[],r=[],i={value:0},o=i.value;return new PathObserver(i,"value",t),e}();document.addEventListener("DOMContentLoaded",function(){L(document),Platform.performMicrotaskCheckpoint()},!1),dt||(e.HTMLTemplateElement=function(){throw TypeError("Illegal constructor")});var ft="__proto__"in{},vt=new $("templateContents"),mt=new $("templateContentsOwner"),gt=new $("templateInstanceRef");HTMLTemplateElement.decorate=function(e,n){if(e.templateIsDecorated_)return!1;var r=e,i=O(r),o=i,a=!i,s=!1;if(!i&&S(r)&&(t(!n),r=D(e),i=O(r),s=!0),r.templateIsDecorated_=!0,!i){x(r);var l=N(r.ownerDocument);vt.set(r,l.createDocumentFragment())}return n?gt.set(r,n):a?A(r,e,s):o&&L(r.content),!0},HTMLTemplateElement.bootstrap=L;var bt=e.HTMLUnknownElement||HTMLElement,yt={get:function(){return vt.get(this)},enumerable:!0,configurable:!0};dt||(HTMLTemplateElement.prototype=Object.create(bt.prototype),Object.defineProperty(HTMLTemplateElement.prototype,"content",yt));var wt=new $("templateModel");_(HTMLTemplateElement.prototype,{bind:function(e,t,n){switch(e){case rt:case it:case ot:var r=St.get(this);r||(r=new G(this),St.set(this,r)),r.inputs.bind(e,t,n||"");break;default:return Element.prototype.bind.call(this,e,t,n)}},unbind:function(e,t,n){switch(e){case rt:case it:case ot:var r=St.get(this);if(!r)break;r.inputs.unbind(e);break;default:return Element.prototype.unbind.call(this,e,t,n)}},unbindAll:function(){this.unbind(rt),this.unbind(it),this.unbind(ot),Element.prototype.unbindAll.call(this)},createInstance:function(){var e=H(this),t=this.getAttribute(at),n=q(e,t);return"function"==typeof HTMLTemplateElement.__instanceCreated&&HTMLTemplateElement.__instanceCreated(n),n},get model(){return wt.get(this)},set model(e){var t=HTMLTemplateElement.syntax[this.getAttribute(at)];wt.set(this,e),U(this,e,t)},get ref(){var e,t=this.getAttribute("ref");return t&&(e=this.ownerDocument.getElementById(t)),e||(e=gt.get(this)),e||null}});var Et=0,Tt=1,Mt=new $("templateInstance");Object.defineProperty(Node.prototype,"templateInstance",{get:function(){var e=Mt.get(this);return e?e:this.parentNode?this.parentNode.templateInstance:void 0}}),V.prototype={set combinator(e){this.combinator_=e,this.scheduleResolve()},bind:function(e,t,n){this.unbind(e),this.size++,this.bindings[e]=new a(t,n,function(t){this.values[e]=t,this.scheduleResolve()}.bind(this))},unbind:function(e,t){this.bindings[e]&&(this.size--,this.bindings[e].dispose(),delete this.bindings[e],delete this.values[e],t||this.scheduleResolve())},scheduleResolve:function(){pt(this.boundResolve)},resolve:function(){if(!this.disposed){if(!this.combinator_)throw Error("CompoundBinding attempted to resolve without a combinator");this.value=this.combinator_(this.values)}},dispose:function(){Object.keys(this.bindings).forEach(function(e){this.unbind(e,!0)},this),this.disposed=!0,this.value=void 0}},G.prototype={resolveInputs:function(e){return ot in e&&!e[ot]?void 0:it in e?e[it]:rt in e?[e[rt]]:void 0},valueChanged:function(e,t){Array.isArray(e)||(e=[]),this.unobserve(),this.iteratedValue=e,this.arrayObserver=new ArrayObserver(this.iteratedValue,this.boundHandleSplices);var n={index:0,addedCount:this.iteratedValue.length,removed:Array.isArray(t)?t:[]};(n.addedCount||n.removed.length)&&this.handleSplices([n])},getTerminatorAt:function(e){if(-1==e)return this.templateElement_;var t=this.terminators[e];if(t.nodeType!==Node.ELEMENT_NODE)return t;var n=St.get(t);return n?n.getTerminatorAt(n.terminators.length-1):t},insertInstanceAt:function(e,t){var n=this.getTerminatorAt(e-1),r=t[t.length-1]||n;this.terminators.splice(e,0,r);for(var i=this.templateElement_.parentNode,o=n.nextSibling,a=0;t.length>a;a++)i.insertBefore(t[a],o)},extractInstanceAt:function(e){var t=[],n=this.getTerminatorAt(e-1),r=this.getTerminatorAt(e);this.terminators.splice(e,1);for(var i=this.templateElement_.parentNode;r!==n;){var o=r;r=o.previousSibling,i.removeChild(o),t.push(o)}return t},getInstanceModel:function(e,t,n){var r=n&&n[lt];return r&&"function"==typeof r?r(e,t):t},getInstanceNodes:function(e,t){var n=[],r=this.templateElement_.createInstance();for(U(r,e,t),W(r,e);r.firstChild;)n.push(r.removeChild(r.firstChild));return n},handleSplices:function(e){var t=this.templateElement_;if(!t.parentNode||!t.ownerDocument.defaultView)return this.abandon(),St.delete(this),void 0; - var n=t.getAttribute(at),r=HTMLTemplateElement.syntax[n],i=new X,o=0;e.forEach(function(e){e.removed.forEach(function(t){var n=this.extractInstanceAt(e.index+o,n);i.set(t,n)},this),o-=e.addedCount},this),e.forEach(function(e){for(var n=e.index;e.index+e.addedCount>n;n++){var o=this.getInstanceModel(t,this.iteratedValue[n],r),a=i.get(o)||this.getInstanceNodes(o,r);this.insertInstanceAt(n,a)}},this),i.forEach(function(e){for(var t=0;e.length>t;t++)B(e[t])})},unobserve:function(){this.arrayObserver&&(this.arrayObserver.close(),this.arrayObserver=void 0)},abandon:function(){this.unobserve(),this.valueBinding.dispose(),this.terminators.length=0,this.inputs.dispose()}};var St=new $("templateIterator");e.CompoundBinding=V,Object.defineProperty(HTMLTemplateElement,at,{value:{},enumerable:!0}),HTMLTemplateElement.forAllTemplatesFrom_=C,HTMLTemplateElement.bindAllMustachesFrom_=U,HTMLTemplateElement.parseAndBind_=j}(this),function(e){function t(){logFlags.data&&console.group("Model.dirtyCheck()"),n(),logFlags.data&&console.groupEnd()}function n(){Platform.performMicrotaskCheckpoint()}document.write(""),HTMLTemplateElement.__instanceCreated=function(e){document.adoptNode(e),CustomElements.upgradeAll(e)};var r=125;window.addEventListener("WebComponentsReady",function(){t(),setInterval(n,r)}),e.flush=t,window.dirtyCheck=t}(window.Platform),function(e){function t(e){return r(e,a)}function n(e){return r(e,"stylesheet")}function r(e,t){return"link"===e.localName&&e.getAttribute("rel")===t}function i(e,t){var n=document.implementation.createHTMLDocument(a);n._URL=t;var r=n.createElement("base");return r.setAttribute("href",document.baseURI),n.head.appendChild(r),n.body.innerHTML=e,n}e||(e=window.HTMLImports={flags:{}});var o,a="import",s={documents:{},cache:{},preloadSelectors:["link[rel="+a+"]","script[src]","link[rel=stylesheet]"].join(","),load:function(e,t){o=new l(s.loaded,t),o.cache=s.cache,s.preload(e)},preload:function(e){var n=e.querySelectorAll(s.preloadSelectors);e===document&&(n=Array.prototype.filter.call(n,function(e){return t(e)})),o.addNodes(n)},loaded:function(e,r,o){if(t(r)){var a=s.documents[e];a||(a=i(o,e),u.resolvePathsInHTML(a),s.documents[e]=a,s.preload(a)),r.content=r.__resource=a}else r.__resource=o,n(r)&&u.resolvePathsInStylesheet(r)}},l=function(e,t){this.onload=e,this.oncomplete=t,this.inflight=0,this.pending={},this.cache={}};l.prototype={addNodes:function(e){this.inflight+=e.length,f(e,this.require,this),this.checkDone()},require:function(e){var t=u.nodeUrl(e);e.__nodeUrl=t,this.dedupe(t,e)||this.fetch(t,e)},dedupe:function(e,t){return this.pending[e]?(this.pending[e].push(t),!0):this.cache[e]?(this.onload(e,t,o.cache[e]),this.tail(),!0):(this.pending[e]=[t],!1)},fetch:function(e,t){p.load(e,function(n,r){this.receive(e,t,n,r)}.bind(this))},receive:function(e,t,n,r){n||(o.cache[e]=r),o.pending[e].forEach(function(t){n||this.onload(e,t,r),this.tail()},this),o.pending[e]=null},tail:function(){--this.inflight,this.checkDone()},checkDone:function(){this.inflight||this.oncomplete()}};var u={nodeUrl:function(e){return u.resolveUrl(u.getDocumentUrl(document),u.hrefOrSrc(e))},hrefOrSrc:function(e){return e.getAttribute("href")||e.getAttribute("src")},documentUrlFromNode:function(e){var t=u.getDocumentUrl(e.ownerDocument);return t=t.split("#")[0]},getDocumentUrl:function(e){return e&&(e._URL||e.impl&&e.impl._URL||e.baseURI||e.URL)||""},resolveUrl:function(e,t,n){if(this.isAbsUrl(t))return t;var r=this.compressUrl(this.urlToPath(e)+t);return n&&(r=u.makeRelPath(u.getDocumentUrl(document),r)),r},isAbsUrl:function(e){return/(^data:)|(^http[s]?:)|(^\/)/.test(e)},urlToPath:function(e){var t=e.split("/");return t.pop(),t.push(""),t.join("/")},compressUrl:function(e){for(var t,n=e.split("/"),r=0;n.length>r;r++)t=n[r],".."===t&&(n.splice(r-1,2),r-=2);return n.join("/")},makeRelPath:function(e,t){var n,r;for(n=this.compressUrl(e).split("/"),r=this.compressUrl(t).split("/");n.length&&n[0]===r[0];)n.shift(),r.shift();for(var i=0,o=n.length-1;o>i;i++)r.unshift("..");var a=r.join("/");return a},resolvePathsInHTML:function(e){var t=u.documentUrlFromNode(e.body);window.HTMLTemplateElement&&HTMLTemplateElement.bootstrap&&HTMLTemplateElement.bootstrap(e);var n=e.body;u._resolvePathsInHTML(n,t)},_resolvePathsInHTML:function(e,t){if(u.resolveAttributes(e,t),u.resolveStyleElts(e,t),window.templateContent){var n=e.querySelectorAll("template");n&&f(n,function(e){u._resolvePathsInHTML(templateContent(e),t)})}},resolvePathsInStylesheet:function(e){var t=u.nodeUrl(e);e.__resource=u.resolveCssText(e.__resource,t)},resolveStyleElts:function(e,t){var n=e.querySelectorAll("style");n&&f(n,function(e){e.textContent=u.resolveCssText(e.textContent,t)})},resolveCssText:function(e,t){return e.replace(/url\([^)]*\)/g,function(e){var n=e.replace(/["']/g,"").slice(4,-1);return n=u.resolveUrl(t,n,!0),"url("+n+")"})},resolveAttributes:function(e,t){var n=e&&e.querySelectorAll(d);n&&f(n,function(e){this.resolveNodeAttributes(e,t)},this)},resolveNodeAttributes:function(e,t){c.forEach(function(n){var r=e.attributes[n];if(r&&r.value&&0>r.value.search(h)){var i=u.resolveUrl(t,r.value,!0);r.value=i}})}},c=["href","src","action"],d="["+c.join("],[")+"]",h="{{.*}}",p={async:!0,ok:function(e){return e.status>=200&&300>e.status||304===e.status},load:function(t,n,r){var i=new XMLHttpRequest;(e.flags.debug||e.flags.bust)&&(t+="?"+Math.random()),i.open("GET",t,p.async),i.addEventListener("readystatechange",function(){4===i.readyState&&n.call(r,!p.ok(i)&&i,i.response,t)}),i.send()}},f=Array.prototype.forEach.call.bind(Array.prototype.forEach);e.importer=s,e.getDocumentUrl=u.getDocumentUrl,"function"!=typeof window.CustomEvent&&(window.CustomEvent=function(e){var t=document.createEvent("HTMLEvents");return t.initEvent(e,!0,!0),t}),window.addEventListener("load",function(){s.load(document,function(){var e=window.ShadowDOMPolyfill?ShadowDOMPolyfill.wrap(document):document;HTMLImports.readyTime=(new Date).getTime(),e.body.dispatchEvent(new CustomEvent("HTMLImportsLoaded",{bubbles:!0}))})})}(window.HTMLImports),function(e){function t(e){w.push(e),y||(y=!0,m(r))}function n(e){return window.ShadowDOMPolyfill&&window.ShadowDOMPolyfill.wrapIfNeeded(e)||e}function r(){y=!1;var e=w;w=[],e.sort(function(e,t){return e.uid_-t.uid_});var t=!1;e.forEach(function(e){var n=e.takeRecords();i(e),n.length&&(e.callback_(n,e),t=!0)}),t&&r()}function i(e){e.nodes_.forEach(function(t){var n=v.get(t);n&&n.forEach(function(t){t.observer===e&&t.removeTransientObservers()})})}function o(e,t){for(var n=e;n;n=n.parentNode){var r=v.get(n);if(r)for(var i=0;r.length>i;i++){var o=r[i],a=o.options;if(n===e||a.subtree){var s=t(a);s&&o.enqueue(s)}}}}function a(e){this.callback_=e,this.nodes_=[],this.records_=[],this.uid_=++E}function s(e,t){this.type=e,this.target=t,this.addedNodes=[],this.removedNodes=[],this.previousSibling=null,this.nextSibling=null,this.attributeName=null,this.attributeNamespace=null,this.oldValue=null}function l(e){var t=new s(e.type,e.target);return t.addedNodes=e.addedNodes.slice(),t.removedNodes=e.removedNodes.slice(),t.previousSibling=e.previousSibling,t.nextSibling=e.nextSibling,t.attributeName=e.attributeName,t.attributeNamespace=e.attributeNamespace,t.oldValue=e.oldValue,t}function u(e,t){return T=new s(e,t)}function c(e){return M?M:(M=l(T),M.oldValue=e,M)}function d(){T=M=void 0}function h(e){return e===M||e===T}function p(e,t){return e===t?e:M&&h(e)?M:null}function f(e,t,n){this.observer=e,this.target=t,this.options=n,this.transientObservedNodes=[]}var v=new SideTable,m=window.msSetImmediate;if(!m){var g=[],b=Math.random()+"";window.addEventListener("message",function(e){if(e.data===b){var t=g;g=[],t.forEach(function(e){e()})}}),m=function(e){g.push(e),window.postMessage(b,"*")}}var y=!1,w=[],E=0;a.prototype={observe:function(e,t){if(e=n(e),!t.childList&&!t.attributes&&!t.characterData||t.attributeOldValue&&!t.attributes||t.attributeFilter&&t.attributeFilter.length&&!t.attributes||t.characterDataOldValue&&!t.characterData)throw new SyntaxError;var r=v.get(e);r||v.set(e,r=[]);for(var i,o=0;r.length>o;o++)if(r[o].observer===this){i=r[o],i.removeListeners(),i.options=t;break}i||(i=new f(this,e,t),r.push(i),this.nodes_.push(e)),i.addListeners()},disconnect:function(){this.nodes_.forEach(function(e){for(var t=v.get(e),n=0;t.length>n;n++){var r=t[n];if(r.observer===this){r.removeListeners(),t.splice(n,1);break}}},this),this.records_=[]},takeRecords:function(){var e=this.records_;return this.records_=[],e}};var T,M;f.prototype={enqueue:function(e){var n=this.observer.records_,r=n.length;if(n.length>0){var i=n[r-1],o=p(i,e);if(o)return n[r-1]=o,void 0}else t(this.observer);n[r]=e},addListeners:function(){this.addListeners_(this.target)},addListeners_:function(e){var t=this.options;t.attributes&&e.addEventListener("DOMAttrModified",this,!0),t.characterData&&e.addEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.addEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.addEventListener("DOMNodeRemoved",this,!0)},removeListeners:function(){this.removeListeners_(this.target)},removeListeners_:function(e){var t=this.options;t.attributes&&e.removeEventListener("DOMAttrModified",this,!0),t.characterData&&e.removeEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.removeEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.removeEventListener("DOMNodeRemoved",this,!0)},addTransientObserver:function(e){if(e!==this.target){this.addListeners_(e),this.transientObservedNodes.push(e);var t=v.get(e);t||v.set(e,t=[]),t.push(this)}},removeTransientObservers:function(){var e=this.transientObservedNodes;this.transientObservedNodes=[],e.forEach(function(e){this.removeListeners_(e);for(var t=v.get(e),n=0;t.length>n;n++)if(t[n]===this){t.splice(n,1);break}},this)},handleEvent:function(e){switch(e.stopImmediatePropagation(),e.type){case"DOMAttrModified":var t=e.attrName,n=e.relatedNode.namespaceURI,r=e.target,i=new u("attributes",r);i.attributeName=t,i.attributeNamespace=n;var a=e.attrChange===MutationEvent.ADDITION?null:e.prevValue;o(r,function(e){return!e.attributes||e.attributeFilter&&e.attributeFilter.length&&-1===e.attributeFilter.indexOf(t)&&-1===e.attributeFilter.indexOf(n)?void 0:e.attributeOldValue?c(a):i});break;case"DOMCharacterDataModified":var r=e.target,i=u("characterData",r),a=e.prevValue;o(r,function(e){return e.characterData?e.characterDataOldValue?c(a):i:void 0});break;case"DOMNodeRemoved":this.addTransientObserver(e.target);case"DOMNodeInserted":var s,l,r=e.relatedNode,h=e.target;"DOMNodeInserted"===e.type?(s=[h],l=[]):(s=[],l=[h]);var p=h.previousSibling,f=h.nextSibling,i=u("childList",r);i.addedNodes=s,i.removedNodes=l,i.previousSibling=p,i.nextSibling=f,o(r,function(e){return e.childList?i:void 0})}d()}},e.JsMutationObserver=a}(this),!window.MutationObserver&&(window.MutationObserver=window.WebKitMutationObserver||window.JsMutationObserver,!MutationObserver))throw Error("no mutation observer support");(function(e){function t(t,o){var a=o||{};if(!t)throw Error("Name argument must not be empty");if(a.name=t,!a.prototype)throw Error("Options missing required prototype property");return a.lifecycle=a.lifecycle||{},a.ancestry=n(a.extends),r(a),i(a),a.prototype.setAttribute=c,a.prototype.removeAttribute=d,p(t,a),a.ctor=f(a),a.ctor.prototype=a.prototype,e.ready&&e.upgradeAll(document),a.ctor}function n(e){var t=w[e];return t?n(t.extends).concat([t]):[]}function r(e){for(var t,n=e.extends,r=0;t=e.ancestry[r];r++)n=t.is&&t.tag;e.tag=n||e.name,n&&(e.is=e.name)}function i(e){if(!Object.__proto__)if(e.is)var t=document.createElement(e.tag),n=Object.getPrototypeOf(t);else n=HTMLElement.prototype;e.native=n}function o(e){return a(E(e.tag),e)}function a(t,n){return n.is&&t.setAttribute("is",n.is),s(t,n),t.__upgraded__=!0,e.upgradeSubtree(t),u(t),t}function s(e,t){Object.__proto__?e.__proto__=t.prototype:(l(e,t.prototype,t.native),e.__proto__=t.prototype)}function l(e,t,n){for(var r={},i=t;i!==n&&i!==HTMLUnknownElement.prototype;){for(var o,a=Object.getOwnPropertyNames(i),s=0;o=a[s];s++)r[o]||(Object.defineProperty(e,o,Object.getOwnPropertyDescriptor(i,o)),r[o]=1);i=Object.getPrototypeOf(i)}}function u(e){e.readyCallback&&e.readyCallback()}function c(e,t){h.call(this,e,t,b)}function d(e,t){h.call(this,e,t,y)}function h(e,t,n){var r=this.getAttribute(e);n.apply(this,arguments),this.attributeChangedCallback&&this.getAttribute(e)!==r&&this.attributeChangedCallback(e,r)}function p(e,t){w[e]=t}function f(e){return function(){return o(e)}}function v(e){var t=w[e];return t?new t.ctor:E(e)}function m(e){if(!e.__upgraded__&&e.nodeType===Node.ELEMENT_NODE){var t=e.getAttribute("is")||e.localName,n=w[t];return n&&a(e,n)}}if(e||(e=window.CustomElements={flags:{}}),e.hasNative=(document.webkitRegister||document.register)&&"native"===e.flags.register,e.hasNative){document.register=document.register||document.webkitRegister;var g=function(){};e.registry={},e.upgradeElement=g}else{var b=HTMLElement.prototype.setAttribute,y=HTMLElement.prototype.removeAttribute,w={},E=document.createElement.bind(document);document.register=t,document.createElement=v,e.registry=w,e.upgrade=m}})(window.CustomElements),function(e){function t(e,n,r){var i=e.firstElementChild;if(!i)for(i=e.firstChild;i&&i.nodeType!==Node.ELEMENT_NODE;)i=i.nextSibling;for(;i;)n(i,r)!==!0&&t(i,n,r),i=i.nextElementSibling;return null}function n(e,r){t(e,function(e){return r(e)?!0:(e.webkitShadowRoot&&n(e.webkitShadowRoot,r),void 0)}),e.webkitShadowRoot&&n(e.webkitShadowRoot,r)}function r(e){return a(e)?(s(e),!0):(l(e),void 0)}function i(e){n(e,function(e){return r(e)?!0:void 0})}function o(e){return r(e)||i(e)}function a(t){if(!t.__upgraded__&&t.nodeType===Node.ELEMENT_NODE){var n=t.getAttribute("is")||t.localName,r=e.registry[n];if(r)return logFlags.dom&&console.group("upgrade:",t.localName),e.upgrade(t),logFlags.dom&&console.groupEnd(),!0}}function s(e){l(e),d(e)&&n(e,function(e){l(e)})}function l(e){(e.insertedCallback||e.__upgraded__&&logFlags.dom)&&(logFlags.dom&&console.group("inserted:",e.localName),d(e)&&(e.__inserted=(e.__inserted||0)+1,1>e.__inserted&&(e.__inserted=1),e.__inserted>1?logFlags.dom&&console.warn("inserted:",e.localName,"insert/remove count:",e.__inserted):e.insertedCallback&&(logFlags.dom&&console.log("inserted:",e.localName),e.insertedCallback())),logFlags.dom&&console.groupEnd())}function u(e){c(e),n(e,function(e){c(e)})}function c(e){(e.removedCallback||e.__upgraded__&&logFlags.dom)&&(logFlags.dom&&console.log("removed:",e.localName),d(e)||(e.__inserted=(e.__inserted||0)-1,e.__inserted>0&&(e.__inserted=0),0>e.__inserted?logFlags.dom&&console.warn("removed:",e.localName,"insert/remove count:",e.__inserted):e.removedCallback&&e.removedCallback()))}function d(e){for(var t=e;t;){if(t==e.ownerDocument)return!0;t=t.parentNode||t.host}}function h(e){e.webkitShadowRoot&&!e.webkitShadowRoot.__watched&&(logFlags.dom&&console.log("watching shadow-root for: ",e.localName),g(e.webkitShadowRoot),e.webkitShadowRoot.__watched=!0)}function p(e){h(e),n(e,function(){h(e)})}function f(e){switch(e.localName){case"style":case"script":case"template":case void 0:return!0}}function v(e){if(logFlags.dom){var t=e[0];if(t&&"childList"===t.type&&t.addedNodes&&t.addedNodes){for(var n=t.addedNodes[0];n&&n!==document&&!n.host;)n=n.parentNode;var r=n&&(n.URL||n._URL||n.host&&n.host.localName)||"";r=r.split("/?").shift().split("/").pop()}console.group("mutations (%d) [%s]",e.length,r||"")}e.forEach(function(e){"childList"===e.type&&(E(e.addedNodes,function(e){f(e)||o(e)}),E(e.removedNodes,function(e){f(e)||u(e)}))}),logFlags.dom&&console.groupEnd()}function m(){v(w.takeRecords())}function g(e){w.observe(e,{childList:!0,subtree:!0})}function b(e){g(e)}function y(e){logFlags.dom&&console.group("upgradeDocument: ",(e.URL||e._URL||"").split("/").pop()),o(e),logFlags.dom&&console.groupEnd()}var w=new MutationObserver(v),E=Array.prototype.forEach.call.bind(Array.prototype.forEach);e.watchShadow=h,e.watchAllShadows=p,e.upgradeAll=o,e.upgradeSubtree=i,e.observeDocument=b,e.upgradeDocument=y,e.takeRecords=m}(window.CustomElements),function(){function parseElementElement(e){var t={name:"","extends":null};takeAttributes(e,t);var n=HTMLElement.prototype;if(t.extends){var r=document.createElement(t.extends);n=r.__proto__||Object.getPrototypeOf(r)}t.prototype=Object.create(n),e.options=t;var i=e.querySelector("script,scripts");i&&executeComponentScript(i.textContent,e,t.name);var o=document.register(t.name,t);e.ctor=o;var a=e.getAttribute("constructor");a&&(window[a]=o)}function takeAttributes(e,t){for(var n in t){var r=e.attributes[n];r&&(t[n]=r.value)}}function executeComponentScript(inScript,inContext,inName){context=inContext;var owner=context.ownerDocument,url=owner._URL||owner.URL||owner.impl&&(owner.impl._URL||owner.impl.URL),match=url.match(/.*\/([^.]*)[.]?.*$/);if(match){var name=match[1];url+=name!=inName?":"+inName:""}var code="__componentScript('"+inName+"', function(){"+inScript+"});"+"\n//@ sourceURL="+url+"\n";eval(code)}function mixin(e){for(var t=e||{},n=1;arguments.length>n;n++){var r=arguments[n];try{for(var i in r)copyProperty(i,r,t)}catch(o){}}return t}function copyProperty(e,t,n){var r=getPropertyDescriptor(t,e);Object.defineProperty(n,e,r)}function getPropertyDescriptor(e,t){if(e){var n=Object.getOwnPropertyDescriptor(e,t);return n||getPropertyDescriptor(Object.getPrototypeOf(e),t)}}var HTMLElementElement=function(e){return e.register=HTMLElementElement.prototype.register,parseElementElement(e),e};HTMLElementElement.prototype={register:function(e){e&&(this.options.lifecycle=e.lifecycle,e.prototype&&mixin(this.options.prototype,e.prototype))}};var context;window.__componentScript=function(e,t){t.call(context)},window.HTMLElementElement=HTMLElementElement,window.mixin=mixin}(),function(){function e(e){return e.ownerDocument===document||e.ownerDocument.impl===document}function t(e){return"link"===e.localName&&e.getAttribute("rel")===r}function n(e){return e.parentNode&&"element"===e.parentNode.localName?!0:void 0}var r="import",i={selectors:["link[rel="+r+"]","link[rel=stylesheet]","script[src]","script","style","element"],map:{link:"parseLink",script:"parseScript",element:"parseElement",style:"parseStyle"},parse:function(e){if(!e.__parsed){e.__parsed=!0;var t=e.querySelectorAll(o.selectors);a(t,function(e){o[o.map[e.localName]](e)}),CustomElements.upgradeDocument(e),CustomElements.observeDocument(e)}},parseLink:function(r){t(r)?r.content&&o.parse(r.content):e(r)||!r.parentNode||n(r)||document.head.appendChild(r)},parseScript:function(t){if(!e(t)&&!n(t)){var r=t.__resource||t.textContent;r&&(r+="\n//@ sourceURL="+t.__nodeUrl+"\n",eval.call(window,r))}},parseStyle:function(t){e(t)||n(t)||document.querySelector("head").appendChild(t)},parseElement:function(e){new HTMLElementElement(e)}},o=i,a=Array.prototype.forEach.call.bind(Array.prototype.forEach);CustomElements.parser=i}(),function(){function e(){setTimeout(function(){CustomElements.parser.parse(document),CustomElements.ready=!0,CustomElements.readyTime=(new Date).getTime(),window.HTMLImports&&(CustomElements.elapsed=CustomElements.readyTime-HTMLImports.readyTime),document.body.dispatchEvent(new CustomEvent("WebComponentsReady",{bubbles:!0}))},0)}"function"!=typeof window.CustomEvent&&(window.CustomEvent=function(e){var t=document.createEvent("HTMLEvents");return t.initEvent(e,!0,!0),t}),window.HTMLImports?document.addEventListener("HTMLImportsLoaded",e):window.addEventListener("load",e)}(),function(){function e(){}if(document.write(""),window.ShadowDOMPolyfill){CustomElements.watchShadow=e,CustomElements.watchAllShadows=e;var t=["upgradeAll","upgradeSubtree","observeDocument","upgradeDocument"],n={};t.forEach(function(e){n[e]=CustomElements[e]}),t.forEach(function(e){CustomElements[e]=function(t){return n[e](wrap(t))}})}}(),function(e){e=e||{};var t={shadow:function(e){return e?e.shadowRoot||e.webkitShadowRoot:void 0},canTarget:function(e){return e&&Boolean(e.elementFromPoint)},targetingShadow:function(e){var t=this.shadow(e);return this.canTarget(t)?t:void 0},searchRoot:function(e,t,n){if(e){var r,i,o,a=e.elementFromPoint(t,n);for(i=this.targetingShadow(a);i;){if(r=i.elementFromPoint(t,n)){var s=this.targetingShadow(r);return this.searchRoot(s,t,n)||r}o=i.querySelector("shadow"),i=o&&o.olderShadowRoot}return a}},findTarget:function(e){var t=e.clientX,n=e.clientY;return this.searchRoot(document,t,n)}};e.targetFinding=t,e.findTarget=t.findTarget.bind(t),window.PointerEventsPolyfill=e}(window.PointerEventsPolyfill),function(){function e(e){return'[touch-action="'+e+'"]'}function t(e){return"{ -ms-touch-action: "+e+"; touch-action: "+e+"; }"}var n=["none","pan-x","pan-y",{rule:"pan-x pan-y",selectors:["scroll","pan-x pan-y","pan-y pan-x"]}],r="";n.forEach(function(n){r+=n+""===n?e(n)+t(n):n.selectors.map(e)+t(n.rule)});var i=document.createElement("style");i.textContent=r;var o=document.querySelector("head");o.insertBefore(i,o.firstChild)}(),function(e){function t(e,t){var t=t||{},i=t.buttons;if(void 0===i)switch(t.which){case 1:i=1;break;case 2:i=4;break;case 3:i=2;break;default:i=0}var o;if(n)o=new MouseEvent(e,t);else{o=document.createEvent("MouseEvent");var a={bubbles:!1,cancelable:!1,view:null,detail:null,screenX:0,screenY:0,clientX:0,clientY:0,ctrlKey:!1,altKey:!1,shiftKey:!1,metaKey:!1,button:0,relatedTarget:null};Object.keys(a).forEach(function(e){e in t&&(a[e]=t[e])}),o.initMouseEvent(e,a.bubbles,a.cancelable,a.view,a.detail,a.screenX,a.screenY,a.clientX,a.clientY,a.ctrlKey,a.altKey,a.shiftKey,a.metaKey,a.button,a.relatedTarget)}r||Object.defineProperty(o,"buttons",{get:function(){return i},enumerable:!0});var s=0;return s=t.pressure?t.pressure:i?.5:0,Object.defineProperties(o,{pointerId:{value:t.pointerId||0,enumerable:!0},width:{value:t.width||0,enumerable:!0},height:{value:t.height||0,enumerable:!0},pressure:{value:s,enumerable:!0},tiltX:{value:t.tiltX||0,enumerable:!0},tiltY:{value:t.tiltY||0,enumerable:!0},pointerType:{value:t.pointerType||"",enumerable:!0},hwTimestamp:{value:t.hwTimestamp||0,enumerable:!0},isPrimary:{value:t.isPrimary||!1,enumerable:!0}}),o}var n=!1,r=!1;try{var i=new MouseEvent("click",{buttons:1});n=!0,r=1===i.buttons}catch(o){}e.PointerEvent=t}(window),function(e){function t(){this.ids=[],this.pointers=[]}t.prototype={set:function(e,t){var n=this.ids.indexOf(e);n>-1?this.pointers[n]=t:(this.ids.push(e),this.pointers.push(t))},has:function(e){return this.ids.indexOf(e)>-1},"delete":function(e){var t=this.ids.indexOf(e);t>-1&&(this.ids.splice(t,1),this.pointers.splice(t,1))},get:function(e){var t=this.ids.indexOf(e);return this.pointers[t]},get size(){return this.pointers.length},clear:function(){this.ids.length=0,this.pointers.length=0}},e.PointerMap=t}(window.PointerEventsPolyfill),function(e){var t;if("undefined"!=typeof WeakMap&&0>navigator.userAgent.indexOf("Firefox/"))t=WeakMap;else{var n=Object.defineProperty,r=Object.hasOwnProperty,i=(new Date).getTime()%1e9;t=function(){this.name="__st"+(1e9*Math.random()>>>0)+(i++ +"__")},t.prototype={set:function(e,t){n(e,this.name,{value:t,writable:!0})},get:function(e){return r.call(e,this.name)?e[this.name]:void 0},"delete":function(e){this.set(e,void 0)}}}e.SideTable=t}(window.PointerEventsPolyfill),function(e){var t={targets:new e.SideTable,handledEvents:new e.SideTable,scrollType:new e.SideTable,pointermap:new e.PointerMap,events:[],eventMap:{},eventSources:{},registerSource:function(e,t){var n=t,r=n.events;r&&(this.events=this.events.concat(r),r.forEach(function(e){n[e]&&(this.eventMap[e]=n[e].bind(n))},this),this.eventSources[e]=n)},registerTarget:function(e,t){this.scrollType.set(e,t||"none"),this.listen(this.events,e,this.boundHandler)},unregisterTarget:function(e){this.scrollType.set(e,null),this.unlisten(this.events,e,this.boundHandler)},down:function(e){this.fireEvent("pointerdown",e)},move:function(e){this.fireEvent("pointermove",e)},up:function(e){this.fireEvent("pointerup",e)},enter:function(e){e.bubbles=!1,this.fireEvent("pointerenter",e)},leave:function(e){e.bubbles=!1,this.fireEvent("pointerleave",e)},over:function(e){e.bubbles=!0,this.fireEvent("pointerover",e)},out:function(e){e.bubbles=!0,this.fireEvent("pointerout",e)},cancel:function(e){this.fireEvent("pointercancel",e)},leaveOut:function(e){e.target.contains(e.relatedTarget)||this.leave(e),this.out(e)},enterOver:function(e){e.target.contains(e.relatedTarget)||this.enter(e),this.over(e)},eventHandler:function(e){if(!this.handledEvents.get(e)){var t=e.type,n=this.eventMap&&this.eventMap[t];n&&n(e),this.handledEvents.set(e,!0)}},listen:function(e,t,n){e.forEach(function(e){this.addEvent(e,n,!1,t)},this)},unlisten:function(e,t,n){e.forEach(function(e){this.removeEvent(e,n,!1,t)},this)},addEvent:function(e,t,n,r){r.addEventListener(e,t,n)},removeEvent:function(e,t,n,r){r.removeEventListener(e,t,n)},makeEvent:function(e,t){var n=new PointerEvent(e,t);return this.targets.set(n,this.targets.get(t)||t.target),n},fireEvent:function(e,t){var n=this.makeEvent(e,t);return this.dispatchEvent(n)},cloneEvent:function(e){var t={};for(var n in e)t[n]=e[n];return t},getTarget:function(e){return this.captureInfo&&this.captureInfo.id===e.pointerId?this.captureInfo.target:this.targets.get(e)},setCapture:function(e,t){this.captureInfo&&this.releaseCapture(this.captureInfo.id),this.captureInfo={id:e,target:t};var n=new PointerEvent("gotpointercapture",{bubbles:!0});this.implicitRelease=this.releaseCapture.bind(this,e),document.addEventListener("pointerup",this.implicitRelease),document.addEventListener("pointercancel",this.implicitRelease),this.targets.set(n,t),this.asyncDispatchEvent(n)},releaseCapture:function(e){if(this.captureInfo&&this.captureInfo.id===e){var t=new PointerEvent("lostpointercapture",{bubbles:!0}),n=this.captureInfo.target;this.captureInfo=null,document.removeEventListener("pointerup",this.implicitRelease),document.removeEventListener("pointercancel",this.implicitRelease),this.targets.set(t,n),this.asyncDispatchEvent(t)}},dispatchEvent:function(e){var t=this.getTarget(e);return t?t.dispatchEvent(e):void 0},asyncDispatchEvent:function(e){setTimeout(this.dispatchEvent.bind(this,e),0)}};t.boundHandler=t.eventHandler.bind(t),e.dispatcher=t}(window.PointerEventsPolyfill),function(e){var t=e.dispatcher,n=Array.prototype.forEach.call.bind(Array.prototype.forEach),r=Array.prototype.map.call.bind(Array.prototype.map),i={ATTRIB:"touch-action",SELECTOR:"[touch-action]",EMITTER:"none",XSCROLLER:"pan-x",YSCROLLER:"pan-y",SCROLLER:/^(?:pan-x pan-y)|(?:pan-y pan-x)|scroll$/,OBSERVER_INIT:{subtree:!0,childList:!0,attributes:!0,attributeFilter:["touch-action"]},watchSubtree:function(t){e.targetFinding.canTarget(t)&&s.observe(t,this.OBSERVER_INIT)},enableOnSubtree:function(e){var t=e||document;this.watchSubtree(e),t===document&&"complete"!==document.readyState?this.installOnLoad():this.installNewSubtree(t)},installNewSubtree:function(e){n(this.findElements(e),this.addElement,this)},findElements:function(e){var t=e||document;return t.querySelectorAll?t.querySelectorAll(this.SELECTOR):[]},touchActionToScrollType:function(e){var t=e;return t===this.EMITTER?"none":t===this.XSCROLLER?"X":t===this.YSCROLLER?"Y":this.SCROLLER.exec(t)?"XY":void 0},removeElement:function(n){t.unregisterTarget(n);var r=e.targetFinding.shadow(n);r&&t.unregisterTarget(r)},addElement:function(n){var r=n.getAttribute&&n.getAttribute(this.ATTRIB),i=this.touchActionToScrollType(r);if(i){t.registerTarget(n,i);var o=e.targetFinding.shadow(n);o&&t.registerTarget(o,i)}},elementChanged:function(e){this.removeElement(e),this.addElement(e)},concatLists:function(e,t){for(var n,r=0,i=t.length;i>r&&(n=t[r]);r++)e.push(n);return e},installOnLoad:function(){document.addEventListener("DOMContentLoaded",this.installNewSubtree.bind(this,document))},flattenMutationTree:function(e){var t=r(e,this.findElements,this);return t.push(e),t.reduce(this.concatLists,[])},mutationWatcher:function(e){e.forEach(this.mutationHandler,this)},mutationHandler:function(e){var t=e;if("childList"===t.type){var n=this.flattenMutationTree(t.addedNodes);n.forEach(this.addElement,this);var r=this.flattenMutationTree(t.removedNodes);r.forEach(this.removeElement,this)}else"attributes"===t.type&&this.elementChanged(t.target)}},o=i.mutationWatcher.bind(i);e.installer=i,e.register=i.enableOnSubtree.bind(i),e.setTouchAction=function(e,n){var r=this.touchActionToScrollType(n);r?t.registerTarget(e,r):t.unregisterTarget(e)}.bind(i);var a=window.MutationObserver||window.WebKitMutationObserver;if(a)var s=new a(o);else i.watchSubtree=function(){console.warn("PointerEventsPolyfill: MutationObservers not found, touch-action will not be dynamically detected")}}(window.PointerEventsPolyfill),function(e){var t=e.dispatcher,n=e.installer,r=e.findTarget,i=t.pointermap,o=t.scrollType,a=Array.prototype.map.call.bind(Array.prototype.map),s=2500,l=25,u={events:["touchstart","touchmove","touchend","touchcancel"],POINTER_TYPE:"touch",firstTouch:null,isPrimaryTouch:function(e){return this.firstTouch===e.identifier},setPrimaryTouch:function(e){null===this.firstTouch&&(this.firstTouch=e.identifier,this.firstXY={X:e.clientX,Y:e.clientY},this.scrolling=!1)},removePrimaryTouch:function(e){this.isPrimaryTouch(e)&&(this.firstTouch=null,this.firstXY=null)},touchToPointer:function(e){var n=t.cloneEvent(e);return n.pointerId=e.identifier+2,n.target=r(n),n.bubbles=!0,n.cancelable=!0,n.button=0,n.buttons=1,n.width=e.webkitRadiusX||e.radiusX,n.height=e.webkitRadiusY||e.radiusY,n.pressure=e.webkitForce||e.force,n.isPrimary=this.isPrimaryTouch(e),n.pointerType=this.POINTER_TYPE,n},processTouches:function(e,t){var n=e.changedTouches,r=a(n,this.touchToPointer,this);r.forEach(t,this)},shouldScroll:function(e){if(this.firstXY){var t,n=o.get(e.currentTarget);if("none"===n)t=!1;else if("XY"===n)t=!0;else{var r=e.changedTouches[0],i=n,a="Y"===n?"X":"Y",s=Math.abs(r["client"+i]-this.firstXY[i]),l=Math.abs(r["client"+a]-this.firstXY[a]);t=s>=l}return this.firstXY=null,t}},findTouch:function(e,t){for(var n,r=0,i=e.length;i>r&&(n=e[r]);r++)if(n.identifier===t)return!0},vacuumTouches:function(e){var t=e.touches;if(i.size>=t.length){var n=[];i.ids.forEach(function(e){if(1!==e&&!this.findTouch(t,e-2)){var r=i.get(e).out;n.push(this.touchToPointer(r))}},this),n.forEach(this.cancelOut,this)}},touchstart:function(e){this.vacuumTouches(e),this.setPrimaryTouch(e.changedTouches[0]),this.dedupSynthMouse(e),this.scrolling||this.processTouches(e,this.overDown)},overDown:function(e){i.set(e.pointerId,{target:e.target,out:e,outTarget:e.target}),t.over(e),t.down(e)},touchmove:function(e){this.scrolling||(this.shouldScroll(e)?(this.scrolling=!0,this.touchcancel(e)):(e.preventDefault(),this.processTouches(e,this.moveOverOut)))},moveOverOut:function(e){var n=e,r=i.get(n.pointerId),o=r.out,a=r.outTarget;t.move(n),o&&a!==n.target&&(o.relatedTarget=n.target,n.relatedTarget=a,o.target=a,t.leaveOut(o),t.enterOver(n)),r.out=n,r.outTarget=n.target},touchend:function(e){this.dedupSynthMouse(e),this.processTouches(e,this.upOut)},upOut:function(e){this.scrolling||(t.up(e),t.out(e)),this.cleanUpPointer(e)},touchcancel:function(e){this.processTouches(e,this.cancelOut)},cancelOut:function(e){t.cancel(e),t.out(e),this.cleanUpPointer(e)},cleanUpPointer:function(e){i.delete(e.pointerId),this.removePrimaryTouch(e)},dedupSynthMouse:function(e){var t=c.lastTouches,n=e.changedTouches[0];if(this.isPrimaryTouch(n)){var r={x:n.clientX,y:n.clientY};t.push(r);var i=function(e,t){var n=e.indexOf(t);n>-1&&e.splice(n,1)}.bind(null,t,r);setTimeout(i,s)}}},c={POINTER_ID:1,POINTER_TYPE:"mouse",events:["mousedown","mousemove","mouseup","mouseover","mouseout"],global:["mousedown","mouseup","mouseover","mouseout"],lastTouches:[],mouseHandler:t.eventHandler.bind(t),isEventSimulatedFromTouch:function(e){for(var t,n=this.lastTouches,r=e.clientX,i=e.clientY,o=0,a=n.length;a>o&&(t=n[o]);o++){var s=Math.abs(r-t.x),u=Math.abs(i-t.y); - if(l>=s&&l>=u)return!0}},prepareEvent:function(e){var n=t.cloneEvent(e);return n.pointerId=this.POINTER_ID,n.isPrimary=!0,n.pointerType=this.POINTER_TYPE,n},mousedown:function(e){if(!this.isEventSimulatedFromTouch(e)){var n=i.has(this.POINTER_ID);if(n&&(this.cancel(e),n=!1),!n){var r=this.prepareEvent(e);i.set(this.POINTER_ID,e),t.down(r),t.listen(this.global,document,this.mouseHandler)}}},mousemove:function(e){if(!this.isEventSimulatedFromTouch(e)){var n=this.prepareEvent(e);t.move(n)}},mouseup:function(e){if(!this.isEventSimulatedFromTouch(e)){var n=i.get(this.POINTER_ID);if(n&&n.button===e.button){var r=this.prepareEvent(e);t.up(r),this.cleanupMouse()}}},mouseover:function(e){if(!this.isEventSimulatedFromTouch(e)){var n=this.prepareEvent(e);t.enterOver(n)}},mouseout:function(e){if(!this.isEventSimulatedFromTouch(e)){var n=this.prepareEvent(e);t.leaveOut(n)}},cancel:function(e){var n=this.prepareEvent(e);t.cancel(n),this.cleanupMouse()},cleanupMouse:function(){i.delete(this.POINTER_ID),t.unlisten(this.global,document,this.mouseHandler)}},d={events:["MSPointerDown","MSPointerMove","MSPointerUp","MSPointerOut","MSPointerOver","MSPointerCancel","MSGotPointerCapture","MSLostPointerCapture"],POINTER_TYPES:["","unavailable","touch","pen","mouse"],prepareEvent:function(e){var n=t.cloneEvent(e);return n.pointerType=this.POINTER_TYPES[e.pointerType],n},cleanup:function(e){i.delete(e)},MSPointerDown:function(e){i.set(e.pointerId,e);var n=this.prepareEvent(e);t.down(n)},MSPointerMove:function(e){var n=this.prepareEvent(e);t.move(n)},MSPointerUp:function(e){var n=this.prepareEvent(e);t.up(n),this.cleanup(e.pointerId)},MSPointerOut:function(e){var n=this.prepareEvent(e);t.leaveOut(n)},MSPointerOver:function(e){var n=this.prepareEvent(e);t.enterOver(n)},MSPointerCancel:function(e){var n=this.prepareEvent(e);t.cancel(n),this.cleanup(e.pointerId)},MSLostPointerCapture:function(e){var n=t.makeEvent("lostpointercapture",e);t.dispatchEvent(n)},MSGotPointerCapture:function(e){var n=t.makeEvent("gotpointercapture",e);t.dispatchEvent(n)}};if(void 0===window.navigator.pointerEnabled){if(window.navigator.msPointerEnabled){var h=window.navigator.msMaxTouchPoints;Object.defineProperty(window.navigator,"maxTouchPoints",{value:h,enumerable:!0}),t.registerSource("ms",d),t.registerTarget(document)}else t.registerSource("mouse",c),"ontouchstart"in window&&t.registerSource("touch",u),n.enableOnSubtree(document),t.listen(["mousemove"],document,t.boundHandler);Object.defineProperty(window.navigator,"pointerEnabled",{value:!0,enumerable:!0})}}(window.PointerEventsPolyfill),function(e){function t(e){if(!i.pointermap.has(e))throw Error("InvalidPointerId")}var n,r,i=e.dispatcher,o=window.navigator;o.msPointerEnabled?(n=function(e){t(e),this.msSetPointerCapture(e)},r=function(e){t(e),this.msReleasePointerCapture(e)}):(n=function(e){t(e),i.setCapture(e,this)},r=function(e){t(e),i.releaseCapture(e,this)}),Element.prototype.setPointerCapture||Object.defineProperties(Element.prototype,{setPointerCapture:{value:n},releasePointerCapture:{value:r}})}(window.PointerEventsPolyfill),PointerGestureEvent.prototype.preventTap=function(){this.tapPrevented=!0},function(e){e=e||{},e.utils={LCA:{find:function(e,t){if(e===t)return e;if(e.contains){if(e.contains(t))return e;if(t.contains(e))return t}var n=this.depth(e),r=this.depth(t),i=n-r;for(i>0?e=this.walk(e,i):t=this.walk(t,-i);e&&t&&e!==t;)e=this.walk(e,1),t=this.walk(t,1);return e},walk:function(e,t){for(var n=0;t>n;n++)e=e.parentNode;return e},depth:function(e){for(var t=0;e;)t++,e=e.parentNode;return t}}},e.findLCA=function(t,n){return e.utils.LCA.find(t,n)},window.PointerGestures=e}(window.PointerGestures),function(e){var t;if("undefined"!=typeof WeakMap&&0>navigator.userAgent.indexOf("Firefox/"))t=WeakMap;else{var n=Object.defineProperty,r=Object.hasOwnProperty,i=(new Date).getTime()%1e9;t=function(){this.name="__st"+(1e9*Math.random()>>>0)+(i++ +"__")},t.prototype={set:function(e,t){n(e,this.name,{value:t,writable:!0})},get:function(e){return r.call(e,this.name)?e[this.name]:void 0},"delete":function(e){this.set(e,void 0)}}}e.SideTable=t}(window.PointerGestures),function(e){function t(){this.ids=[],this.pointers=[]}t.prototype={set:function(e,t){var n=this.ids.indexOf(e);n>-1?this.pointers[n]=t:(this.ids.push(e),this.pointers.push(t))},has:function(e){return this.ids.indexOf(e)>-1},"delete":function(e){var t=this.ids.indexOf(e);t>-1&&(this.ids.splice(t,1),this.pointers.splice(t,1))},get:function(e){var t=this.ids.indexOf(e);return this.pointers[t]},get size(){return this.pointers.length},clear:function(){this.ids.length=0,this.pointers.length=0}},window.Map&&(t=window.Map),e.PointerMap=t}(window.PointerGestures),function(e){var t={handledEvents:new e.SideTable,targets:new e.SideTable,handlers:{},recognizers:{},events:["pointerdown","pointermove","pointerup","pointerover","pointerout","pointercancel"],registerRecognizer:function(e,t){var n=t;this.recognizers[e]=n,this.events.forEach(function(e){if(n[e]){var t=n[e].bind(n);this.addHandler(e,t)}},this)},addHandler:function(e,t){var n=e;this.handlers[n]||(this.handlers[n]=[]),this.handlers[n].push(t)},registerTarget:function(e){this.listen(this.events,e)},unregisterTarget:function(e){this.unlisten(this.events,e)},eventHandler:function(e){if(!this.handledEvents.get(e)){var t,n=e.type;(t=this.handlers[n])&&this.makeQueue(t,e),this.handledEvents.set(e,!0)}},makeQueue:function(e,t){var n=this.cloneEvent(t);setTimeout(this.runQueue.bind(this,e,n),0)},runQueue:function(e,t){this.currentPointerId=t.pointerId;for(var n,r=0,i=e.length;i>r&&(n=e[r]);r++)n(t);this.currentPointerId=0},listen:function(e,t){e.forEach(function(e){this.addEvent(e,this.boundHandler,!1,t)},this)},unlisten:function(e){e.forEach(function(e){this.removeEvent(e,this.boundHandler,!1,inTarget)},this)},addEvent:function(e,t,n,r){r.addEventListener(e,t,n)},removeEvent:function(e,t,n,r){r.removeEventListener(e,t,n)},makeEvent:function(e,t){return new PointerGestureEvent(e,t)},cloneEvent:function(e){var t={};for(var n in e)t[n]=e[n];return t},dispatchEvent:function(e,t){var n=t||this.targets.get(e);n&&(n.dispatchEvent(e),e.tapPrevented&&this.preventTap(this.currentPointerId))},asyncDispatchEvent:function(e,t){var n=function(){this.dispatchEvent(e,t)}.bind(this);setTimeout(n,0)},preventTap:function(e){var t=this.recognizers.tap;t&&t.preventTap(e)}};t.boundHandler=t.eventHandler.bind(t),e.dispatcher=t,e.register=function(t){var n=window.PointerEventsPolyfill;n&&n.register(t),e.dispatcher.registerTarget(t)},t.registerTarget(document)}(window.PointerGestures),function(e){var t=e.dispatcher,n={HOLD_DELAY:200,WIGGLE_THRESHOLD:16,events:["pointerdown","pointermove","pointerup","pointercancel"],heldPointer:null,holdJob:null,pulse:function(){var e=Date.now()-this.heldPointer.timeStamp,t=this.held?"holdpulse":"hold";this.fireHold(t,e),this.held=!0},cancel:function(){clearInterval(this.holdJob),this.held&&this.fireHold("release"),this.held=!1,this.heldPointer=null,this.target=null,this.holdJob=null},pointerdown:function(e){e.isPrimary&&!this.heldPointer&&(this.heldPointer=e,this.target=e.target,this.holdJob=setInterval(this.pulse.bind(this),this.HOLD_DELAY))},pointerup:function(e){this.heldPointer&&this.heldPointer.pointerId===e.pointerId&&this.cancel()},pointercancel:function(){this.cancel()},pointermove:function(e){if(this.heldPointer&&this.heldPointer.pointerId===e.pointerId){var t=e.clientX-this.heldPointer.clientX,n=e.clientY-this.heldPointer.clientY;t*t+n*n>this.WIGGLE_THRESHOLD&&this.cancel()}},fireHold:function(e,n){var r={pointerType:this.heldPointer.pointerType};n&&(r.holdTime=n);var i=t.makeEvent(e,r);t.dispatchEvent(i,this.target),i.tapPrevented&&t.preventTap(this.heldPointer.pointerId)}};t.registerRecognizer("hold",n)}(window.PointerGestures),function(e){var t=e.dispatcher,n=new e.PointerMap,r={events:["pointerdown","pointermove","pointerup","pointercancel"],WIGGLE_THRESHOLD:4,clampDir:function(e){return e>0?1:-1},calcPositionDelta:function(e,t){var n=0,r=0;return e&&t&&(n=t.pageX-e.pageX,r=t.pageY-e.pageY),{x:n,y:r}},fireTrack:function(e,n,r){var i=r,o=this.calcPositionDelta(i.downEvent,n),a=this.calcPositionDelta(i.lastMoveEvent,n);a.x&&(i.xDirection=this.clampDir(a.x)),a.y&&(i.yDirection=this.clampDir(a.y));var s={dx:o.x,dy:o.y,ddx:a.x,ddy:a.y,clientX:n.clientX,clientY:n.clientY,pageX:n.pageX,pageY:n.pageY,screenX:n.screenX,screenY:n.screenY,xDirection:i.xDirection,yDirection:i.yDirection,trackInfo:i.trackInfo,pointerType:n.pointerType};"trackend"===e&&(s._releaseTarget=n.target);var l=t.makeEvent(e,s);i.lastMoveEvent=n,t.dispatchEvent(l,i.downTarget)},pointerdown:function(e){if(e.isPrimary&&("mouse"===e.pointerType?1===e.buttons:!0)){var t={downEvent:e,downTarget:e.target,trackInfo:{},lastMoveEvent:null,xDirection:0,yDirection:0,tracking:!1};n.set(e.pointerId,t)}},pointermove:function(e){var t=n.get(e.pointerId);if(t)if(t.tracking)this.fireTrack("track",e,t);else{var r=this.calcPositionDelta(t.downEvent,e),i=r.x*r.x+r.y*r.y;i>this.WIGGLE_THRESHOLD&&(t.tracking=!0,this.fireTrack("trackstart",t.downEvent,t),this.fireTrack("track",e,t))}},pointerup:function(e){var t=n.get(e.pointerId);t&&(t.tracking&&this.fireTrack("trackend",e,t),n.delete(e.pointerId))},pointercancel:function(e){this.pointerup(e)}};t.registerRecognizer("track",r)}(window.PointerGestures),function(e){var t=e.dispatcher,n={MIN_VELOCITY:.5,MAX_QUEUE:4,moveQueue:[],target:null,pointerId:null,events:["pointerdown","pointermove","pointerup","pointercancel"],pointerdown:function(e){e.isPrimary&&!this.pointerId&&(this.pointerId=e.pointerId,this.target=e.target,this.addMove(e))},pointermove:function(e){e.pointerId===this.pointerId&&this.addMove(e)},pointerup:function(e){e.pointerId===this.pointerId&&this.fireFlick(e),this.cleanup()},pointercancel:function(){this.cleanup()},cleanup:function(){this.moveQueue=[],this.target=null,this.pointerId=null},addMove:function(e){this.moveQueue.length>=this.MAX_QUEUE&&this.moveQueue.shift(),this.moveQueue.push(e)},fireFlick:function(e){for(var n,r,i,o,a,s,l,u=e,c=this.moveQueue.length,d=0,h=0,p=0,f=0;c>f&&(l=this.moveQueue[f]);f++)n=u.timeStamp-l.timeStamp,r=u.clientX-l.clientX,i=u.clientY-l.clientY,o=r/n,a=i/n,s=Math.sqrt(o*o+a*a),s>p&&(d=o,h=a,p=s);var v=Math.abs(d)>Math.abs(h)?"x":"y",m=this.calcAngle(d,h);if(Math.abs(p)>=this.MIN_VELOCITY){var g=t.makeEvent("flick",{xVelocity:d,yVelocity:h,velocity:p,angle:m,majorAxis:v,pointerType:e.pointerType});t.dispatchEvent(g,this.target)}},calcAngle:function(e,t){return 180*Math.atan2(t,e)/Math.PI}};t.registerRecognizer("flick",n)}(window.PointerGestures),function(e){var t=e.dispatcher,n=new e.PointerMap,r={events:["pointerdown","pointermove","pointerup","pointercancel"],pointerdown:function(e){e.isPrimary&&!e.tapPrevented&&n.set(e.pointerId,{target:e.target,x:e.clientX,y:e.clientY})},pointermove:function(e){if(e.isPrimary){var t=n.get(e.pointerId);t&&e.tapPrevented&&n.delete(e.pointerId)}},pointerup:function(r){var i=n.get(r.pointerId);if(i&&!r.tapPrevented){var o=e.findLCA(i.target,r.target);if(o){var a=t.makeEvent("tap",{x:r.clientX,y:r.clientY,pointerType:r.pointerType});t.dispatchEvent(a,o)}}n.delete(r.pointerId)},pointercancel:function(e){n.delete(e.pointerId)},preventTap:function(e){n.delete(e)}};t.registerRecognizer("tap",r)}(window.PointerGestures),function(){var e=Array.prototype.forEach.call.bind(Array.prototype.forEach);window.forEach=e}(),function(){function e(e,n){1==arguments.length&&(n=e,e=null),n&&n.hasOwnProperty("constructor")||(n.constructor=function(){this.super()});var r=n.constructor,o=e&&e.prototype||Object.prototype;return r.prototype=t(o,n),"super"in r.prototype||(r.prototype.super=i),r}function t(e,t){return Object.create(e,n(t))}function n(e){var t={};for(var n in e)t[n]=r(e,n);return t}function r(e,t){return e&&Object.getOwnPropertyDescriptor(e,t)||r(Object.getPrototypeOf(e),t)}function i(e){var t=i.caller,n=t._nom;if(!n&&(n=t._nom=s.call(this,t),!n))return console.warn('called super() on a method not in "this"'),void 0;"_super"in t||a(t,n,Object.getPrototypeOf(this));var r=t._super;if(r){var o=r[n];return"_super"in o||a(o,n,r),o.apply(this,e||[])}}function o(e,t,n){for(var r=e;r&&(!r.hasOwnProperty(t)||r[t]==n);)r=Object.getPrototypeOf(r);return r}function a(e,t,n){e._super=o(n,t,e),e._super&&(e._super[t]._nom=t)}function s(e){for(var t in this){var n=r(this,t);if(n.value==e)return t}}window.$class=e,window.extend=t,window.$super=i}(),function(){function e(e,r){if(e!=window){if(!(e&&e instanceof HTMLElement))throw"First argument to Polymer.register must be an HTMLElement";var i=mixin({},Polymer.base,r);i.elementElement=e,Polymer.addResolvePath(i,e),i.installTemplate=function(){this.super(),n.call(this,e)},i.readyCallback=t,Polymer.parseHostEvents(e.attributes,i),Polymer.publishAttributes(e,i),Polymer.installSheets(e),Polymer.shimStyling(e),e.register({prototype:i}),logFlags.comps&&console.log("Polymer: element registered"+e.options.name)}}function t(){this.installTemplate(),i.call(this)}function n(e){var t=e.querySelector("template");if(t){var n=this.webkitCreateShadowRoot();return n.applyAuthorStyles=this.applyAuthorStyles,CustomElements.watchShadow(this),n.host=this,n.appendChild(t.createInstance()),PointerGestures.register(n),PointerEventsPolyfill.setTouchAction(n,this.getAttribute("touch-action")),r.call(this,n),n}}function r(e){CustomElements.takeRecords(),Polymer.bindModel.call(this,e),Polymer.marshalNodeReferences.call(this,e);var t=Polymer.accumulateEvents(e);Polymer.bindAccumulatedLocalEvents.call(this,e,t)}function i(){Polymer.observeProperties.call(this),Polymer.takeAttributes.call(this);var e=Polymer.accumulateHostEvents.call(this);Polymer.bindAccumulatedHostEvents.call(this,e),this.ready&&this.ready()}function o(e,t){for(var n=e;n&&n!=this;){var r=Array.prototype.indexOf.call(t,n);if(r>=0)return r;n=n.parentNode}}window.logFlags||{},window.Polymer={register:e,findDistributedTarget:o,instanceReady:i}}(),function(e){var t=window.logFlags||{},n={"super":$super,isPolymerElement:!0,bind:function(){Polymer.bind.apply(this,arguments)},unbind:function(){Polymer.unbind.apply(this,arguments)},job:function(){return Polymer.job.apply(this,arguments)},asyncMethod:function(e,t,n){var r=t&&t.length?t:[t];return window.setTimeout(function(){(this[e]||e).apply(this,r)}.bind(this),n||0)},dispatch:function(e,t){this[e]&&this[e].apply(this,t)},fire:function(e,n,r){var i=r||this;return t.events&&console.log("[%s]: sending [%s]",i.localName,e),i.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:n})),n},asend:function(){this.asyncMethod("send",arguments)},classFollows:function(e,t,n){t&&t.classList.remove(n),e&&e.classList.add(n)}};n.send=n.fire,e.base=n}(window.Polymer),function(){function e(e,n,r,i){t.bind&&console.log("[%s]: bindProperties: [%s] to [%s].[%s]",r.localName||"object",i,e.localName,n);var o=PathObserver.getValueAtPath(r,i);(null==o||void 0===o)&&PathObserver.setValueAtPath(r,i,e[n]),Object.defineProperty(e,n,{get:function(){return PathObserver.getValueAtPath(r,i)},set:function(e){PathObserver.setValueAtPath(r,i,e)},configurable:!0,enumerable:!0})}var t=window.logFlags||{};Polymer.bindProperties=e}(),function(){function e(e,t,n){var r=u.get(e);r||u.set(e,r={}),r[t.toLowerCase()]=n}function t(e,t){var n=u.get(e);n&&delete n[t.toLowerCase()]}function n(n){var r=n.prototype,i=r.bind,o=r.unbind;r.bind=function(t,n,r){i.apply(this,arguments),e(this,t,r)},r.unbind=function(e){o.apply(this,arguments),t(this,e)}}function r(e){return e&&u.get(e)||c}function i(e,t){return r(e)[t.toLowerCase()]}function o(e){l.bind&&console.group("[%s] bindModel",this.localName),HTMLTemplateElement.bindAllMustachesFrom_(e,this),l.bind&&console.groupEnd()}function a(t,n,r){var i=Polymer.propertyForAttribute.call(this,t);i?(e(this,i,r),Polymer.bindProperties(this,i,n,r)):HTMLElement.prototype.bind.apply(this,arguments)}function s(e){var n=Polymer.propertyForAttribute.call(this,e);n?(t(this,e),Object.defineProperty(this,e,{value:this[e],enumerable:!0,writable:!0,configurable:!0})):HTMLElement.prototype.unbind.apply(this,arguments)}var l=window.logFlags||{},u=new SideTable;[Node,Element,Text,HTMLInputElement].forEach(n);var c={},d=/\{\{([^{}]*)}}/;Polymer.bind=a,Polymer.unbind=s,Polymer.getBinding=i,Polymer.bindModel=o,Polymer.bindPattern=d}(),function(){function e(){forEach(this.attributes,function(e){var i=t.call(this,e.name);if(i){if(e.value.search(r)>=0)return;var o=this[i],a=n(e.value,o);a!==o&&(this[i]=a)}},this)}function t(e){var t=Object.keys(this[i]);return t[t.map(l).indexOf(e.toLowerCase())]}function n(e,t){var n=typeof t;switch(t instanceof Date&&(n="date"),n){case"string":return e;case"date":return new Date(Date.parse(e)||Date.now());case"boolean":if(""==e)return!0}switch(e){case"true":return!0;case"false":return!1}var r=parseFloat(e);return r+""===e?r:e}var r=Polymer.bindPattern,i="__published",o="attributes",a="publish",s=function(e,t){var n={},r=e.getAttribute(o);if(r){var s=r.split(r.indexOf(",")>=0?",":" ");s.forEach(function(e){e=e.trim(),e&&(n[e]=null)})}var l=e.options.prototype;Object.keys(n).forEach(function(e){e in t||e in l||(t[e]=n[e])});var u=t[a];u&&(Object.keys(u).forEach(function(e){t[e]=u[e]}),n=mixin(n,u)),t[i]=mixin({},l[i],n)},l=String.prototype.toLowerCase.call.bind(String.prototype.toLowerCase);Polymer.takeAttributes=e,Polymer.publishAttributes=s,Polymer.propertyForAttribute=t}(),Polymer.marshalNodeReferences=function(e){var t=this.$=this.$||{};if(e){var n=e.querySelectorAll("[id]");forEach(n,function(e){t[e.id]=e})}},function(){function e(e,t,n){var r=n.bind(this);for(var i in t)l.events&&console.log('[%s] bindAccumulatedEvents: addEventListener("%s", listen)',e.localName||"root",i),e.addEventListener(i,r)}function t(t){e.call(this,this,t,i)}function n(t,n){e.call(this,t,n,r)}function r(e){if(!e.cancelBubble){e.on=u+e.type,l.events&&console.group("[%s]: listenLocal [%s]",this.localName,e.on);for(var t=e.target;t&&t!=this;){var n=w(t);if(n&&a.call(n,t,e))return;t=t.parentNode}l.events&&console.groupEnd()}}function i(e){e.cancelBubble||(l.events&&console.group("[%s]: listenHost [%s]",this.localName,e.type),s.call(this,this,e),l.events&&console.groupEnd())}function o(e){var t=T.get(e);return t||(t=[],T.set(e,t)),t}function a(e,t){if(e.attributes){var n=o(t);if(0>n.indexOf(e)){n.push(e);var r=e.getAttribute(t.on);r&&(l.events&&console.log("[%s] found handler name [%s]",this.localName,r),E(this,r,[t,t.detail,e]))}}return t.cancelBubble}function s(e,t){var n=M.call(e,t.type);return n&&(l.events&&console.log("[%s] found host handler name [%s]",e.localName,n),E(e,n,[t,t.detail,e])),t.cancelBubble}var l=window.logFlags||{},u="on-",c=function(e,t){t.eventDelegates=d(e)},d=function(e){var t={};if(e)for(var n,r=0;n=e[r];r++)n.name.slice(0,u.length)==u&&(t[n.name.slice(u.length)]=n.value);return t},h=function(e,t){var n=t||{};return p(e,n),m(e,n),g(e,n),n},p=function(e,t){var n=e.attributes;if(n)for(var r,i=0;r=n[i];i++)r.name.slice(0,u.length)===u&&v(r.name.slice(u.length),t)},f={webkitanimationstart:"webkitAnimationStart",webkitanimationend:"webkitAnimationEnd",webkittransitionend:"webkitTransitionEnd",domfocusout:"DOMFocusOut",domfocusin:"DOMFocusIn"},v=function(e,t){var n=f[e]||e;t[n]=1},m=function(e,t){for(var n,r=e.childNodes,i=0;n=r[i];i++)h(n,t)},g=function(e,t){if("template"==e.localName){var n=b(e);n&&m(n,t)}},b=function(e){return e.ref?e.ref.content:e.content},y=function(e){for(var t=e||{},n=this.__proto__;n&&n!==HTMLElement.prototype;){if(n.hasOwnProperty("eventDelegates"))for(var r in n.eventDelegates)v(r,t);n=n.__proto__}return t},w=function(e){for(var t=e;t.parentNode&&"shadow-root"!==t.localName;)t=t.parentNode;return t.host},E=function(e,t,n){e&&(l.events&&console.group("[%s] dispatch [%s]",e.localName,t),e.dispatch(t,n),l.events&&console.groupEnd())},T=new SideTable("handledList"),M=function(e){for(var t=this;t;){if(t.hasOwnProperty("eventDelegates")){var n=t.eventDelegates[e]||t.eventDelegates[e.toLowerCase()];if(n)return n}t=t.__proto__}};Polymer.parseHostEvents=c,Polymer.accumulateEvents=h,Polymer.accumulateHostEvents=y,Polymer.bindAccumulatedHostEvents=t,Polymer.bindAccumulatedLocalEvents=n}(),function(){function e(){for(var e in this)t.call(this,e)}function t(e){n.call(this,e)&&(i.observe&&console.log("["+this.localName+"] watching ["+e+"]"),new PathObserver(this,e,function(t,n){i.data&&console.log("[%s#%s] watch: [%s] now [%s] was [%s]",this.localName,this.node.id||"",e,this[e],n),r.call(this,e,n)}.bind(this)))}function n(e){return"_"!=e[0]&&!(e in Object.prototype)&&Boolean(this[e+o])}function r(e,t){var n=e+o;this[n]&&this[n](t)}var i=window.logFlags||{},o="Changed";Polymer.observeProperties=e}(),function(){function e(e){t(e),n(e)}function t(e){var t=e.querySelectorAll("[rel=stylesheet]"),n=e.querySelector("template");if(n)var r=templateContent(n);r&&f(t,function(e){if(!e.hasAttribute(p)){e.parentNode.removeChild(e);var t=o(e);t&&r.insertBefore(t,r.firstChild)}})}function n(e){var t=e.globalStyles||(e.globalStyles=l(e,"global"));a(t,u.head)}function r(e,t){var n=t.controllerStyles||(t.controllerStyles=l(t,"controller"));c.queue(function(){var t=i(e);t&&(Polymer.shimPolyfillDirectives(n,e.localName),a(n,t))})}function i(e){for(var t=e;t.parentNode&&"shadow-root"!=t.localName;)t=t.parentNode;return t==u?u.head:t}function o(e){if(e.__resource){var t=u.createElement("style");return t.textContent=e.__resource,t}console.warn("Could not find content for stylesheet",e)}function a(e,t){e.forEach(function(e){t.appendChild(e.cloneNode(!0))})}function s(e,t){return h?h.call(e,t):void 0}function l(e,t){var n=[],r=e.querySelectorAll("[rel=stylesheet]"),i="["+p+"="+t+"]";Array.prototype.forEach.call(r,function(e){s(e,i)&&(e.parentNode.removeChild(e),n.push(o(e)))});var a=e.querySelectorAll("style");return Array.prototype.forEach.call(a,function(e){s(e,i)&&(e.parentNode.removeChild(e),n.push(e))}),n}window.logFlags||{};var u=window.ShadowDOMPolyfill?ShadowDOMPolyfill.wrap(document):document,c={list:[],queue:function(e){e&&c.list.push(e),c.queueFlush()},queueFlush:function(){c.flushing||(c.flushing=!0,requestAnimationFrame(c.flush))},flush:function(){c.list.forEach(function(e){e()}),c.list=[],c.flushing=!1}},d=HTMLElement.prototype,h=d.matches||d.matchesSelector||d.webkitMatchesSelector||d.mozMatchesSelector,p="polymer-scope",f=Array.prototype.forEach.call.bind(Array.prototype.forEach);Polymer.installSheets=e,Polymer.installControllerStyles=r}(),function(){var e=Array.prototype.forEach.call.bind(Array.prototype.forEach),t=Array.prototype.concat.call.bind(Array.prototype.concat),n=Array.prototype.slice.call.bind(Array.prototype.slice),r={hostRuleRe:/@host[^{]*{(([^}]*?{[^{]*?}[\s\S]*?)+)}/gim,selectorRe:/([^{]*)({[\s\S]*?})/gim,hostFixableRe:/^[.\[:]/,cssCommentRe:/\/\*[^*]*\*+([^/*][^*]*\*+)*\//gim,cssPolyfillCommentRe:/\/\*\s*@polyfill ([^*]*\*+([^/*][^*]*\*+)*\/)([^{]*?){/gim,selectorReSuffix:"([>\\s~+[.,{:][\\s\\S]*)?$",hostRe:/@host/gim,cache:{},shimStyling:function(e){if(window.ShadowDOMPolyfill&&e){var t=e.options.name;r.cacheDefinition(e),r.shimPolyfillDirectives(e.styles,t),r.applyShimming(r.stylesForElement(e),t)}},shimShadowDOMStyling:function(e,t){window.ShadowDOMPolyfill&&(r.shimPolyfillDirectives(e,t),r.applyShimming(e,t))},applyShimming:function(e,t){this.shimAtHost(e,t),this.shimScoping(e,t)},cacheDefinition:function(e){var t=e.options.name,i=e.querySelector("template"),o=i&&templateContent(i),a=o&&o.querySelectorAll("style");e.styles=a?n(a):[],e.templateContent=o,r.cache[t]=e},stylesForElement:function(e){var r=e.styles,i=e.templateContent&&e.templateContent.querySelector("shadow");if(i||null===e.templateContent){var o=this.findExtendee(e.options.name);if(o){var a=this.stylesForElement(o);r=t(n(a),n(r))}}return r},findExtendee:function(e){var t=this.cache[e];return t&&this.cache[t.options.extends]},shimPolyfillDirectives:function(t,n){window.ShadowDOMPolyfill&&t&&e(t,function(e){e.textContent=this.convertPolyfillDirectives(e.textContent,n)},this)},shimAtHost:function(e,t){if(e){var n=this.convertAtHostStyles(e,t);this.addCssToDocument(n)}},shimScoping:function(e,t){e&&this.applyPseudoScoping(e,t)},convertPolyfillDirectives:function(e){for(var t,n="",r=0;t=this.cssPolyfillCommentRe.exec(e);)n+=e.substring(r,t.index),n+=t[1].slice(0,-2)+"{",r=this.cssPolyfillCommentRe.lastIndex;return n+=e.substring(r,e.length)},findAtHostRules:function(e,t){return Array.prototype.filter.call(e,this.isHostRule.bind(this,t))},isHostRule:function(e,t){return t.selectorText&&t.selectorText.match(e)||t.cssRules&&this.findAtHostRules(t.cssRules,e).length||t.type==CSSRule.WEBKIT_KEYFRAMES_RULE},convertAtHostStyles:function(e,t){for(var n,r=this.stylesToCssText(e),i="",o=0;n=this.hostRuleRe.exec(r);)i+=r.substring(o,n.index),i+=this.scopeHostCss(n[1],t),o=this.hostRuleRe.lastIndex;i+=r.substring(o,r.length);var a=RegExp("^"+t+this.selectorReSuffix,"m"),r=this.rulesToCss(this.findAtHostRules(this.cssToRules(i),a));return r},scopeHostCss:function(e,t){for(var n,r="";n=this.selectorRe.exec(e);)r+=this.scopeHostSelector(n[1],t)+" "+n[2]+"\n ";return r},scopeHostSelector:function(e,t){var n=[],r=e.split(",");return r.forEach(function(e){e=e.trim(),e.indexOf("*")>=0?e=e.replace("*",t):e.match(this.hostFixableRe)&&(e=t+e),n.push(e)},this),n.join(", ")},applyPseudoScoping:function(t,n){e(t,function(e){e.parentNode&&e.parentNode.removeChild(e)});var r=this.stylesToCssText(t).replace(this.hostRuleRe,""),i=this.cssToRules(r),r=this.pseudoScopeRules(i,n);this.addCssToDocument(r)},pseudoScopeRules:function(t,n){var r="";return e(t,function(e){e.selectorText&&e.style&&e.style.cssText?(r+=this.pseudoScopeSelector(e.selectorText,n)+" {\n ",r+=e.style.cssText+"\n}\n\n"):e.media?(r+="@media "+e.media.mediaText+" {\n",r+=this.pseudoScopeRules(e.cssRules,n),r+="\n}\n\n"):e.cssText&&(r+=e.cssText+"\n\n")},this),r},pseudoScopeSelector:function(e,t){var n=[],r=e.split(",");return r.forEach(function(e){n.push(t+" "+e.trim())}),n.join(", ")},stylesToCssText:function(t,n){var r="";return e(t,function(e){r+=e.textContent+"\n\n"}),n||(r=this.stripCssComments(r)),r},stripCssComments:function(e){return e.replace(this.cssCommentRe,"")},cssToRules:function(e){var t=document.createElement("style");t.textContent=e,document.head.appendChild(t);var n=t.sheet.cssRules;return t.parentNode.removeChild(t),n},rulesToCss:function(e){for(var t=0,n=[];e.length>t;t++)n.push(e[t].cssText);return n.join("\n\n")},addCssToDocument:function(e){e&&this.getSheet().appendChild(document.createTextNode(e))},getSheet:function(){return this.sheet||(this.sheet=document.createElement("style")),this.sheet},apply:function(){this.addCssToDocument("style { display: none !important; }\n"),document.head.appendChild(this.getSheet())}};document.addEventListener("WebComponentsReady",function(){r.apply()}),Polymer.shimStyling=r.shimStyling,Polymer.shimShadowDOMStyling=r.shimShadowDOMStyling,Polymer.shimPolyfillDirectives=r.shimPolyfillDirectives.bind(r)}(window),function(){function e(e,t){var r=n(t);e.resolvePath=function(e){return r+e}}function t(e){if(e){var t=e.split("/");return t.pop(),t.push(""),t.join("/")}return""}function n(e){return t(HTMLImports.getDocumentUrl(e.ownerDocument))}Polymer.addResolvePath=e}(),function(){function e(e,n,r){var i=e||new t(this);return i.stop(),i.go(n,r),i}var t=function(e){this.context=e};t.prototype={go:function(e,t){this.callback=e,this.handle=setTimeout(function(){this.handle=null,e.call(this.context)}.bind(this),t)},stop:function(){this.handle&&(clearTimeout(this.handle),this.handle=null)},complete:function(){this.handle&&(this.stop(),this.callback.call(this.context))}},Polymer.job=e}(),function(){document.write("\n"),document.write("\n"),document.write('\n'),document.write('\n'),document.write("\n"),document.write(""),document.write("\n"),window.addEventListener("WebComponentsReady",function(){document.body.style.webkitTransition="opacity 0.3s",document.body.style.opacity=1})}(); -//@ sourceMappingURL=polymer.min.js.map \ No newline at end of file diff --git a/bower_components/handsontable/demo/web_component/x-tile.html b/bower_components/handsontable/demo/web_component/x-tile.html deleted file mode 100644 index 3fe75c20..00000000 --- a/bower_components/handsontable/demo/web_component/x-tile.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - diff --git a/bower_components/handsontable/dist/README.md b/bower_components/handsontable/dist/README.md index efbb1a24..db8029db 100644 --- a/bower_components/handsontable/dist/README.md +++ b/bower_components/handsontable/dist/README.md @@ -19,9 +19,8 @@ If you are a "Bob the Builder" kind of hacker, you will need to load Handsontabl ```html - + - ``` diff --git a/bower_components/handsontable/dist/jquery.handsontable.css b/bower_components/handsontable/dist/jquery.handsontable.css deleted file mode 100644 index 53da4ae5..00000000 --- a/bower_components/handsontable/dist/jquery.handsontable.css +++ /dev/null @@ -1,384 +0,0 @@ -/** - * Handsontable 0.9.19 - * Handsontable is a simple jQuery plugin for editable tables with basic copy-paste compatibility with Excel and Google Docs - * - * Copyright 2012, Marcin Warpechowski - * Licensed under the MIT license. - * http://handsontable.com/ - * - * Date: Tue Oct 01 2013 13:17:18 GMT+0200 (Central European Daylight Time) - */ - -.handsontable { - position: relative; - font-family: Arial, Helvetica, sans-serif; - line-height: 1.3em; - font-size: 13px; -} - -.handsontable.htAutoColumnSize { - visibility: hidden; - left: 0; - position: absolute; - top: 0; -} - -.handsontable table, -.handsontable tbody, -.handsontable thead, -.handsontable td, -.handsontable th, -.handsontable div -{ - box-sizing: content-box; - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; -} - -.handsontable table.htCore { - border-collapse: separate; /*it must be separate, otherwise there are offset miscalculations in WebKit: http://stackoverflow.com/questions/2655987/border-collapse-differences-in-ff-and-webkit*/ - position: relative; - /*this actually only changes appearance of user selection - does not make text unselectable - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -o-user-select: none; - -ms-user-select: none; - /*user-select: none; /*no browser supports unprefixed version*/ - border-spacing: 0; - margin: 0; - border-width: 0; - table-layout: fixed; - width: 0; - outline-width: 0; - /* reset bootstrap table style. for more info see: https://github.com/warpech/jquery-handsontable/issues/224 */ - max-width: none; - max-height: none; -} - -.handsontable col { - width: 50px; -} - -.handsontable col.rowHeader { - width: 50px; -} - -.handsontable th, -.handsontable td { - border-right: 1px solid #CCC; - border-bottom: 1px solid #CCC; - height: 22px; - empty-cells: show; - line-height: 21px; - padding: 0 4px 0 4px; /* top, bottom padding different than 0 is handled poorly by FF with HTML5 doctype */ - background-color: #FFF; - font-size: 12px; - vertical-align: top; - overflow: hidden; - outline-width: 0; - white-space: pre-line; /* preserve new line character in cell */ -} - -.handsontable td.htInvalid { - -webkit-transition: background 0.75s ease; - transition: background 0.75s ease; - background-color: #ff4c42; -} - -.handsontable th:last-child { - /*Foundation framework fix*/ - border-right: 1px solid #CCC; - border-bottom: 1px solid #CCC; -} - -.handsontable tr:first-child th.htNoFrame, -.handsontable th:first-child.htNoFrame, -.handsontable th.htNoFrame { - border-left-width: 0; - background-color: white; - border-color: #FFF; -} - -.handsontable th:first-child, -.handsontable td:first-child, -.handsontable .htNoFrame + th, -.handsontable .htNoFrame + td { - border-left: 1px solid #CCC; -} - -.handsontable tr:first-child th, -.handsontable tr:first-child td { - border-top: 1px solid #CCC; -} - -.handsontable thead tr:last-child th { - border-bottom-width: 0; -} - -.handsontable thead tr.lastChild th { - border-bottom-width: 0; -} - -.handsontable th { - background-color: #EEE; - color: #222; - text-align: center; - font-weight: normal; - white-space: nowrap; -} - -.handsontable th .small { - font-size: 12px; -} - -.handsontable thead th { - padding: 0; -} - -.handsontable th.active { - background-color: #CCC; -} - -.handsontable thead th .relative { - position: relative; - padding: 2px 4px; -} - -/* plugins */ - -.handsontable .manualColumnMover { - position: absolute; - left: 0; - top: 0; - background-color: transparent; - width: 5px; - height: 25px; - z-index: 999; - cursor: move; -} - -.handsontable th .manualColumnMover:hover, -.handsontable th .manualColumnMover.active { - background-color: #88F; -} - -.handsontable .manualColumnResizer { - position: absolute; - top: 0; - cursor: col-resize; -} - -.handsontable .manualColumnResizerHandle { - background-color: transparent; - width: 5px; - height: 25px; -} - -.handsontable .manualColumnResizer:hover .manualColumnResizerHandle, -.handsontable .manualColumnResizer.active .manualColumnResizerHandle { - background-color: #AAB; -} - -.handsontable .manualColumnResizerLine { - position: absolute; - right: 0; - top: 0; - background-color: #AAB; - display: none; - width: 0; - border-right: 1px dashed #777; -} - -.handsontable .manualColumnResizer.active .manualColumnResizerLine { - display: block; -} - -.handsontable .columnSorting:hover { - text-decoration: underline; - cursor: pointer; -} - -/* border line */ -.handsontable .wtBorder { - position: absolute; - font-size: 0; -} - -.handsontable td.area { - background-color: #EEF4FF; -} - -/* fill handle */ -.handsontable .wtBorder.corner { - font-size: 0; - cursor: crosshair; -} - -.handsontable .htBorder.htFillBorder { - background: red; - width: 1px; - height: 1px; -} - -.handsontableInput { - border: 2px solid #5292F7; - outline-width: 0; - margin: 0; - padding: 1px 4px 0 2px; - font-family: Arial, Helvetica, sans-serif; /*repeat from .handsontable (inherit doesn't work with IE<8) */ - line-height: 1.3em; /*repeat from .handsontable (inherit doesn't work with IE<8) */ - font-size: 13px; - -webkit-box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.4); - box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.4); - resize: none; - - /*below are needed to overwrite stuff added by jQuery UI Bootstrap theme*/ - display: inline-block; - font-size: 13px; - color: #000; - border-radius: 0; -} - -.handsontableInputHolder { - position: absolute; - top: 0; - left: 0; - width: 1px; - height: 1px; -} - -/* -TextRenderer readOnly cell -*/ -.handsontable .htDimmed { - font-style: italic; - color: #777; -} - -/* -AutocompleteRenderer down arrow -*/ -.handsontable .htAutocomplete { - position: relative; - padding-right: 20px; -} - -.handsontable .htAutocompleteArrow { - position: absolute; - top: 0; - right: 0; - font-size: 10px; - color: #EEE; - cursor: default; - width: 16px; - text-align: center; -} - -.handsontable td .htAutocompleteArrow:hover { - color: #777; -} - -/* -CheckboxRenderer -*/ -.handsontable .htCheckboxRendererInput.noValue { - opacity: 0.5; -} - -/* -NumericRenderer -*/ -.handsontable .htNumeric { - text-align: right; -} - -/* typeahead rules. Needed only if you are using the autocomplete feature */ -.handsontable .typeahead { - position: absolute; - font-family: Arial, Helvetica, sans-serif; - line-height: 1.3em; - font-size: 13px; - z-index: 10; - top: 100%; - left: 0; - float: left; - display: none; - min-width: 160px; - padding: 4px 0; - margin: 2px 0 0 0; - list-style: none; - background-color: white; - border-color: #CCC; - border-color: rgba(0, 0, 0, 0.2); - border-style: solid; - border-width: 1px; - -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - -webkit-background-clip: padding-box; - background-clip: padding-box; - border-radius: 4px; -} - -.handsontable .typeahead li { - line-height: 18px; - min-height: 18px; - display: list-item; - margin: 0; -} - -.handsontable .typeahead a { - display: block; - padding: 3px 15px; - clear: both; - font-weight: normal; - line-height: 18px; - min-height: 18px; - color: #333; - white-space: nowrap; -} - -.handsontable .typeahead li > a:hover, -.handsontable .typeahead .active > a, -.handsontable .typeahead .active > a:hover { - color: white; - text-decoration: none; - background-color: #08C; -} - -.handsontable .typeahead a { - color: #08C; - text-decoration: none; -} - -/*context menu rules*/ -ul.context-menu-list { - color: black; -} - -ul.context-menu-list li { - margin-bottom: 0; /*Foundation framework fix*/ -} - -/** - * dragdealer - */ - -.handsontable .dragdealer { - position: relative; - width: 9px; - height: 9px; - background: #F8F8F8; - border: 1px solid #DDD; -} - -.handsontable .dragdealer .handle { - position: absolute; - width: 9px; - height: 9px; - background: #C5C5C5; -} - -.handsontable .dragdealer .disabled { - background: #898989; -} \ No newline at end of file diff --git a/bower_components/handsontable/dist/jquery.handsontable.full.css b/bower_components/handsontable/dist/jquery.handsontable.full.css deleted file mode 100644 index 956d8eb9..00000000 --- a/bower_components/handsontable/dist/jquery.handsontable.full.css +++ /dev/null @@ -1,642 +0,0 @@ -/** - * Handsontable 0.11.0-beta3 - * Handsontable is a simple jQuery plugin for editable tables with basic copy-paste compatibility with Excel and Google Docs - * - * Copyright 2012-2014 Marcin Warpechowski - * Licensed under the MIT license. - * http://handsontable.com/ - * - * Date: Tue Aug 19 2014 15:47:45 GMT+0200 (CEST) - */ - -.handsontable { - position: relative; -} - -.handsontable .relative { - position: relative; -} - -.handsontable.htAutoColumnSize { - visibility: hidden; - left: 0; - position: absolute; - top: 0; -} - -.handsontable table, -.handsontable tbody, -.handsontable thead, -.handsontable td, -.handsontable th, -.handsontable div { - box-sizing: content-box; - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; -} - -.handsontable table.htCore { - border-collapse: separate; - /*it must be separate, otherwise there are offset miscalculations in WebKit: http://stackoverflow.com/questions/2655987/border-collapse-differences-in-ff-and-webkit*/ - position: relative; - /*this actually only changes appearance of user selection - does not make text unselectable - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -o-user-select: none; - -ms-user-select: none; - /*user-select: none; /*no browser supports unprefixed version*/ - border-spacing: 0; - margin: 0; - border-width: 0; - table-layout: fixed; - width: 0; - outline-width: 0; - /* reset bootstrap table style. for more info see: https://github.com/handsontable/jquery-handsontable/issues/224 */ - max-width: none; - max-height: none; -} - -.handsontable col { - width: 50px; -} - -.handsontable col.rowHeader { - width: 50px; -} - -.handsontable th, -.handsontable td { - border-right: 1px solid #CCC; - border-bottom: 1px solid #CCC; - height: 22px; - empty-cells: show; - line-height: 21px; - padding: 0 4px 0 4px; - /* top, bottom padding different than 0 is handled poorly by FF with HTML5 doctype */ - background-color: #FFF; - vertical-align: top; - overflow: hidden; - outline-width: 0; - white-space: pre-line; - /* preserve new line character in cell */ -} - -.handsontable td.htInvalid { - -webkit-transition: background 0.75s ease; - transition: background 0.75s ease; - background-color: #ff4c42; -} - -.handsontable td.htNoWrap { - white-space: nowrap; -} - -.handsontable th:last-child { - /*Foundation framework fix*/ - border-right: 1px solid #CCC; - border-bottom: 1px solid #CCC; -} - -.handsontable tr:first-child th.htNoFrame, -.handsontable th:first-child.htNoFrame, -.handsontable th.htNoFrame { - border-left-width: 0; - background-color: white; - border-color: #FFF; -} - -.handsontable th:first-child, -.handsontable td:first-child, -.handsontable .htNoFrame + th, -.handsontable .htNoFrame + td { - border-left: 1px solid #CCC; -} - -.handsontable tr:first-child th, -.handsontable tr:first-child td { - border-top: 1px solid #CCC; -} - -.handsontable thead tr:last-child th { - border-bottom-width: 0; -} - -.handsontable thead tr.lastChild th { - border-bottom-width: 0; -} - -.handsontable th { - background-color: #EEE; - color: #222; - text-align: center; - font-weight: normal; - white-space: nowrap; -} - -.handsontable thead th { - padding: 0; -} - -.handsontable th.active { - background-color: #CCC; -} - -.handsontable thead th .relative { - padding: 2px 4px; -} - -/* plugins */ - -.handsontable .manualColumnMover { - position: fixed; - left: 0; - top: 0; - background-color: transparent; - width: 5px; - height: 25px; - z-index: 999; - cursor: move; -} - -.handsontable .manualRowMover { - position: fixed; - left: -4px; - top: 0; - background-color: transparent; - height: 5px; - width: 50px; - z-index: 999; - cursor: move; -} - -.handsontable .manualColumnMoverGuide, -.handsontable .manualRowMoverGuide { - position: fixed; - left: 0; - top: 0; - background-color: #CCC; - width: 25px; - height: 25px; - opacity: 0.7; - display: none; -} - -.handsontable .manualColumnMoverGuide.active, -.handsontable .manualRowMoverGuide.active { - display: block; -} - -.handsontable .manualColumnMover:hover, -.handsontable .manualColumnMover.active, -.handsontable .manualRowMover:hover, -.handsontable .manualRowMover.active{ - background-color: #88F; -} - -/* row + column resizer*/ - -.handsontable .manualColumnResizer { - position: fixed; - top: 0; - cursor: col-resize; - z-index: 110; - width: 5px; - height: 25px; -} - -.handsontable .manualRowResizer { - position: fixed; - left: 0; - cursor: row-resize; - z-index: 110; - height: 5px; - width: 50px; -} - -.handsontable .manualColumnResizer:hover, -.handsontable .manualColumnResizer.active, -.handsontable .manualRowResizer:hover, -.handsontable .manualRowResizer.active { - background-color: #AAB; -} - -.handsontable .manualColumnResizerGuide { - position: fixed; - right: 0; - top: 0; - background-color: #AAB; - display: none; - width: 0; - border-right: 1px dashed #777; - margin-left: 5px; -} - -.handsontable .manualRowResizerGuide { - position: fixed; - left: 0; - bottom: 0; - background-color: #AAB; - display: none; - height: 0; - border-bottom: 1px dashed #777; - margin-top: 5px; -} - -.handsontable .manualColumnResizerGuide.active, -.handsontable .manualRowResizerGuide.active { - display: block; -} - -.handsontable .columnSorting:hover { - text-decoration: underline; - cursor: pointer; -} - -/* border line */ - -.handsontable .wtBorder { - position: absolute; - font-size: 0; -} -.handsontable .wtBorder.hidden{ - display:none !important; -} - -.handsontable td.area { - background-color: #EEF4FF; -} - -/* fill handle */ - -.handsontable .wtBorder.corner { - font-size: 0; - cursor: crosshair; -} - -.handsontable .htBorder.htFillBorder { - background: red; - width: 1px; - height: 1px; -} - -.handsontableInput { - border: 2px solid #5292F7; - outline-width: 0; - margin: 0; - padding: 1px 4px 0 2px; - font-family: Arial, Helvetica, sans-serif; - /*repeat from .handsontable (inherit doesn't work with IE<8) */ - line-height: 1.3em; - /*repeat from .handsontable (inherit doesn't work with IE<8) */ - font-size: inherit; - -webkit-box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.4); - box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.4); - resize: none; - /*below are needed to overwrite stuff added by jQuery UI Bootstrap theme*/ - display: inline-block; - color: #000; - border-radius: 0; - background-color: #FFF; - /*overwrite styles potentionally made by a framework*/ -} - -.handsontableInputHolder { - position: absolute; - top: 0; - left: 0; - z-index: 100; -} - -.htSelectEditor { - -webkit-appearance: menulist-button !important; - position: absolute; -} - -/* -TextRenderer readOnly cell -*/ - -.handsontable .htDimmed { - color: #777; -} - -.handsontable .htSubmenu :after{ - content: '▶'; - color: #777; - position: absolute; - right: 5px; -} - - -/* -TextRenderer horizontal alignment -*/ -.handsontable .htLeft{ - text-align: left; -} -.handsontable .htCenter{ - text-align: center; -} -.handsontable .htRight{ - text-align: right; -} -.handsontable .htJustify{ - text-align: justify; -} -/* -TextRenderer vertical alignment -*/ -.handsontable .htTop{ - vertical-align: top; -} -.handsontable .htMiddle{ - vertical-align: middle; -} -.handsontable .htBottom{ - vertical-align: bottom; -} - -/* -TextRenderer placeholder value -*/ - -.handsontable .htPlaceholder { - color: #999; -} - -/* -AutocompleteRenderer down arrow -*/ - -.handsontable .htAutocompleteArrow { - float: right; - font-size: 10px; - color: #EEE; - cursor: default; - width: 16px; - text-align: center; -} - -.handsontable td .htAutocompleteArrow:hover { - color: #777; -} - -/* -CheckboxRenderer -*/ - -.handsontable .htCheckboxRendererInput.noValue { - opacity: 0.5; -} - -/* -NumericRenderer -*/ - -.handsontable .htNumeric { - text-align: right; -} - -/* -Comment For Cell -*/ -.htCommentCell{ - position: relative; -} -.htCommentCell:after{ - content: ''; - position: absolute; - top: 0; - right: 0; - border-left: 6px solid transparent; - border-top: 6px solid red; -} - -/** - * Handsontable in Handsontable - */ - -.handsontable .handsontable .wtHider { - padding: 0 0 5px 0; -} - -.handsontable .handsontable table { - -webkit-box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.4); - box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.4); -} - -/** -* Autocomplete Editor -*/ -.handsontable .autocompleteEditor.handsontable { - padding-right: 15px; -} - -/** - * Handsontable listbox theme - */ - -.handsontable.listbox { - margin: 0; -} - -.handsontable.listbox .ht_master table { - border: 1px solid #ccc; - border-collapse: separate; - background: white; -} - -.handsontable.listbox th, -.handsontable.listbox tr:first-child th, -.handsontable.listbox tr:last-child th, -.handsontable.listbox tr:first-child td, -.handsontable.listbox td { - border-width: 0; -} - -.handsontable.listbox th, -.handsontable.listbox td { - white-space: nowrap; - text-overflow: ellipsis; -} - -.handsontable.listbox td.htDimmed { - cursor: default; - color: inherit; - font-style: inherit; -} - -.handsontable.listbox .wtBorder { - visibility: hidden; -} - -.handsontable.listbox tr td.current, -.handsontable.listbox tr:hover td { - background: #eee; -} - -.htContextMenu { - display: none; - position: absolute; - z-index: 900; -} - -.htContextMenu .ht_clone_top, -.htContextMenu .ht_clone_left, -.htContextMenu .ht_clone_corner { - display: none; -} - -.ht_clone_top { - z-index: 101; -} - -.ht_clone_left { - z-index: 102; -} - -.ht_clone_corner { - z-index: 103; -} - -.htContextMenu table.htCore { - outline: 1px solid #bbb; -} - -.htContextMenu .wtBorder { - visibility: hidden; -} - -.htContextMenu table tbody tr td { - background: white; - border-width: 0; - padding: 4px 6px 0px 6px; - cursor: pointer; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - -.htContextMenu table tbody tr td:first-child { - border: 0; -} - -.htContextMenu table tbody tr td.htDimmed{ - font-style: normal; - color: #323232; -} - -.htContextMenu table tbody tr td.current{ - background: rgb(233, 233, 233); -} - -.htContextMenu table tbody tr td.htSeparator { - border-top: 1px solid #bbb; - height: 0; - padding: 0; -} - -.htContextMenu table tbody tr td.htDisabled { - color: #999; -} - -.htContextMenu table tbody tr td.htDisabled:hover { - background: white; - color: #999; - cursor: default; -} -.htContextMenu table tbody tr td div{ - padding-left: 10px; -} -.htContextMenu table tbody tr td div span.selected{ - margin-top: -2px; - position: absolute; - left: 4px; -} - -.handsontable td.htSearchResult { - background: #fcedd9; - color: #583707; -} - -/* -Cell borders -*/ -.htBordered{ - /*box-sizing: border-box !important;*/ - border-width: 1px; -} -.htBordered.htTopBorderSolid{ - border-top-style: solid; - border-top-color: #000; -} -.htBordered.htRightBorderSolid{ - border-right-style: solid; - border-right-color: #000; -} -.htBordered.htBottomBorderSolid{ - border-bottom-style: solid; - border-bottom-color: #000; -} -.htBordered.htLeftBorderSolid{ - border-left-style: solid; - border-left-color: #000; -} - -.htCommentTextArea{ - background-color: #FFFACD; - box-shadow: 1px 1px 2px #bbb; - font-family: 'Arial'; - -webkit-box-shadow: 1px 1px 2px #bbb; - -moz-box-shadow: 1px 1px 2px #bbb; - -} - -/*WalkontableDebugOverlay*/ - -.wtDebugHidden { - display: none; -} - -.wtDebugVisible { - display: block; - -webkit-animation-duration: 0.5s; - -webkit-animation-name: wtFadeInFromNone; - animation-duration: 0.5s; - animation-name: wtFadeInFromNone; -} - -@keyframes wtFadeInFromNone { - 0% { - display: none; - opacity: 0; - } - - 1% { - display: block; - opacity: 0; - } - - 100% { - display: block; - opacity: 1; - } -} - -@-webkit-keyframes wtFadeInFromNone { - 0% { - display: none; - opacity: 0; - } - - 1% { - display: block; - opacity: 0; - } - - 100% { - display: block; - opacity: 1; - } -} \ No newline at end of file diff --git a/bower_components/handsontable/dist/jquery.handsontable.full.js b/bower_components/handsontable/dist/jquery.handsontable.full.js deleted file mode 100644 index 3e09b100..00000000 --- a/bower_components/handsontable/dist/jquery.handsontable.full.js +++ /dev/null @@ -1,16426 +0,0 @@ -/** - * Handsontable 0.11.0-beta3 - * Handsontable is a simple jQuery plugin for editable tables with basic copy-paste compatibility with Excel and Google Docs - * - * Copyright 2012-2014 Marcin Warpechowski - * Licensed under the MIT license. - * http://handsontable.com/ - * - * Date: Tue Aug 19 2014 15:47:45 GMT+0200 (CEST) - */ -/*jslint white: true, browser: true, plusplus: true, indent: 4, maxerr: 50 */ - -var Handsontable = { //class namespace - plugins: {}, //plugin namespace - helper: {} //helper namespace -}; - -(function ($, window, Handsontable) { - "use strict"; -//http://stackoverflow.com/questions/3629183/why-doesnt-indexof-work-on-an-array-ie8 -if (!Array.prototype.indexOf) { - Array.prototype.indexOf = function (elt /*, from*/) { - var len = this.length >>> 0; - - var from = Number(arguments[1]) || 0; - from = (from < 0) - ? Math.ceil(from) - : Math.floor(from); - if (from < 0) - from += len; - - for (; from < len; from++) { - if (from in this && - this[from] === elt) - return from; - } - return -1; - }; -} -/** - * Array.filter() shim by Trevor Menagh (https://github.com/trevmex) with some modifications - */ - -if (!Array.prototype.filter) { - Array.prototype.filter = function (fun, thisp) { - "use strict"; - - if (typeof this === "undefined" || this === null) { - throw new TypeError(); - } - if (typeof fun !== "function") { - throw new TypeError(); - } - - thisp = thisp || this; - - if (isNodeList(thisp)) { - thisp = convertNodeListToArray(thisp); - } - - var len = thisp.length, - res = [], - i, - val; - - for (i = 0; i < len; i += 1) { - if (thisp.hasOwnProperty(i)) { - val = thisp[i]; // in case fun mutates this - if (fun.call(thisp, val, i, thisp)) { - res.push(val); - } - } - } - - return res; - - function isNodeList(object) { - return /NodeList/i.test(object.item); - } - - function convertNodeListToArray(nodeList) { - var array = []; - - for (var i = 0, len = nodeList.length; i < len; i++){ - array[i] = nodeList[i] - } - - return array; - } - }; -} - -/* - * Copyright 2012 The Polymer Authors. All rights reserved. - * Use of this source code is governed by a BSD-style - * license that can be found in the LICENSE file. - */ - -if (typeof WeakMap === 'undefined') { - (function() { - var defineProperty = Object.defineProperty; - - try { - var properDefineProperty = true; - defineProperty(function(){}, 'foo', {}); - } catch (e) { - properDefineProperty = false; - } - - /* - IE8 does not support Date.now() but IE8 compatibility mode in IE9 and IE10 does. - M$ deserves a high five for this one :) - */ - var counter = +(new Date) % 1e9; - - var WeakMap = function() { - this.name = '__st' + (Math.random() * 1e9 >>> 0) + (counter++ + '__'); - if(!properDefineProperty){ - this._wmCache = []; - } - }; - - if(properDefineProperty){ - WeakMap.prototype = { - set: function(key, value) { - var entry = key[this.name]; - if (entry && entry[0] === key) - entry[1] = value; - else - defineProperty(key, this.name, {value: [key, value], writable: true}); - - }, - get: function(key) { - var entry; - return (entry = key[this.name]) && entry[0] === key ? - entry[1] : undefined; - }, - 'delete': function(key) { - this.set(key, undefined); - } - }; - } else { - WeakMap.prototype = { - set: function(key, value) { - - if(typeof key == 'undefined' || typeof value == 'undefined') return; - - for(var i = 0, len = this._wmCache.length; i < len; i++){ - if(this._wmCache[i].key == key){ - this._wmCache[i].value = value; - return; - } - } - - this._wmCache.push({key: key, value: value}); - - }, - get: function(key) { - - if(typeof key == 'undefined') return; - - for(var i = 0, len = this._wmCache.length; i < len; i++){ - if(this._wmCache[i].key == key){ - return this._wmCache[i].value; - } - } - - return; - - }, - 'delete': function(key) { - - if(typeof key == 'undefined') return; - - for(var i = 0, len = this._wmCache.length; i < len; i++){ - if(this._wmCache[i].key == key){ - Array.prototype.slice.call(this._wmCache, i, 1); - } - } - } - }; - } - - window.WeakMap = WeakMap; - })(); -} - -Handsontable.activeGuid = null; - -/** - * Handsontable constructor - * @param rootElement The jQuery element in which Handsontable DOM will be inserted - * @param userSettings - * @constructor - */ -Handsontable.Core = function (rootElement, userSettings) { - var priv - , datamap - , grid - , selection - , editorManager - , instance = this - , GridSettings = function () {} - , $document = $(document.documentElement) - , $body = $(document.body); - - Handsontable.helper.extend(GridSettings.prototype, DefaultSettings.prototype); //create grid settings as a copy of default settings - Handsontable.helper.extend(GridSettings.prototype, userSettings); //overwrite defaults with user settings - Handsontable.helper.extend(GridSettings.prototype, expandType(userSettings)); - - this.rootElement = rootElement; - - this.container = document.createElement('DIV'); - this.container.className = 'htContainer'; - rootElement.prepend(this.container); - this.container = $(this.container); - - this.guid = 'ht_' + Handsontable.helper.randomString(); //this is the namespace for global events - - if (!this.rootElement[0].id) { - this.rootElement[0].id = this.guid; //if root element does not have an id, assign a random id - } - - priv = { - cellSettings: [], - columnSettings: [], - columnsSettingConflicts: ['data', 'width'], - settings: new GridSettings(), // current settings instance - selRange: null, //exposed by public method `getSelectedRange` - isPopulated: null, - scrollable: null, - firstRun: true - }; - - grid = { - /** - * Inserts or removes rows and columns - * @param {String} action Possible values: "insert_row", "insert_col", "remove_row", "remove_col" - * @param {Number} index - * @param {Number} amount - * @param {String} [source] Optional. Source of hook runner. - * @param {Boolean} [keepEmptyRows] Optional. Flag for preventing deletion of empty rows. - */ - alter: function (action, index, amount, source, keepEmptyRows) { - var delta; - - amount = amount || 1; - - switch (action) { - case "insert_row": - delta = datamap.createRow(index, amount); - - if (delta) { - if (selection.isSelected() && priv.selRange.from.row >= index) { - priv.selRange.from.row = priv.selRange.from.row + delta; - selection.transformEnd(delta, 0); //will call render() internally - } - else { - selection.refreshBorders(); //it will call render and prepare methods - } - } - break; - - case "insert_col": - delta = datamap.createCol(index, amount); - - if (delta) { - - if(Handsontable.helper.isArray(instance.getSettings().colHeaders)){ - var spliceArray = [index, 0]; - spliceArray.length += delta; //inserts empty (undefined) elements at the end of an array - Array.prototype.splice.apply(instance.getSettings().colHeaders, spliceArray); //inserts empty (undefined) elements into the colHeader array - } - - if (selection.isSelected() && priv.selRange.from.col >= index) { - priv.selRange.from.col = priv.selRange.from.col + delta; - selection.transformEnd(0, delta); //will call render() internally - } - else { - selection.refreshBorders(); //it will call render and prepare methods - } - } - break; - - case "remove_row": - datamap.removeRow(index, amount); - priv.cellSettings.splice(index, amount); - grid.adjustRowsAndCols(); - selection.refreshBorders(); //it will call render and prepare methods - break; - - case "remove_col": - datamap.removeCol(index, amount); - - for(var row = 0, len = datamap.getAll().length; row < len; row++){ - if(row in priv.cellSettings){ //if row hasn't been rendered it wouldn't have cellSettings - priv.cellSettings[row].splice(index, amount); - } - } - - if(Handsontable.helper.isArray(instance.getSettings().colHeaders)){ - if(typeof index == 'undefined'){ - index = -1; - } - instance.getSettings().colHeaders.splice(index, amount); - } - - priv.columnSettings.splice(index, amount); - - grid.adjustRowsAndCols(); - selection.refreshBorders(); //it will call render and prepare methods - break; - - default: - throw new Error('There is no such action "' + action + '"'); - break; - } - - if (!keepEmptyRows) { - grid.adjustRowsAndCols(); //makes sure that we did not add rows that will be removed in next refresh - } - }, - - /** - * Makes sure there are empty rows at the bottom of the table - */ - adjustRowsAndCols: function () { - var r, rlen, emptyRows, emptyCols; - - //should I add empty rows to data source to meet minRows? - rlen = instance.countRows(); - if (rlen < priv.settings.minRows) { - for (r = 0; r < priv.settings.minRows - rlen; r++) { - datamap.createRow(instance.countRows(), 1, true); - } - } - - emptyRows = instance.countEmptyRows(true); - - //should I add empty rows to meet minSpareRows? - if (emptyRows < priv.settings.minSpareRows) { - for (; emptyRows < priv.settings.minSpareRows && instance.countRows() < priv.settings.maxRows; emptyRows++) { - datamap.createRow(instance.countRows(), 1, true); - } - } - - //count currently empty cols - emptyCols = instance.countEmptyCols(true); - - //should I add empty cols to meet minCols? - if (!priv.settings.columns && instance.countCols() < priv.settings.minCols) { - for (; instance.countCols() < priv.settings.minCols; emptyCols++) { - datamap.createCol(instance.countCols(), 1, true); - } - } - - //should I add empty cols to meet minSpareCols? - if (!priv.settings.columns && instance.dataType === 'array' && emptyCols < priv.settings.minSpareCols) { - for (; emptyCols < priv.settings.minSpareCols && instance.countCols() < priv.settings.maxCols; emptyCols++) { - datamap.createCol(instance.countCols(), 1, true); - } - } - - // if (priv.settings.enterBeginsEditing) { - // for (; (((priv.settings.minRows || priv.settings.minSpareRows) && instance.countRows() > priv.settings.minRows) && (priv.settings.minSpareRows && emptyRows > priv.settings.minSpareRows)); emptyRows--) { - // datamap.removeRow(); - // } - // } - - // if (priv.settings.enterBeginsEditing && !priv.settings.columns) { - // for (; (((priv.settings.minCols || priv.settings.minSpareCols) && instance.countCols() > priv.settings.minCols) && (priv.settings.minSpareCols && emptyCols > priv.settings.minSpareCols)); emptyCols--) { - // datamap.removeCol(); - // } - // } - - var rowCount = instance.countRows(); - var colCount = instance.countCols(); - - if (rowCount === 0 || colCount === 0) { - selection.deselect(); - } - - if (selection.isSelected()) { - var selectionChanged; - var fromRow = priv.selRange.from.row; - var fromCol = priv.selRange.from.col; - var toRow = priv.selRange.to.row; - var toCol = priv.selRange.to.col; - - //if selection is outside, move selection to last row - if (fromRow > rowCount - 1) { - fromRow = rowCount - 1; - selectionChanged = true; - if (toRow > fromRow) { - toRow = fromRow; - } - } else if (toRow > rowCount - 1) { - toRow = rowCount - 1; - selectionChanged = true; - if (fromRow > toRow) { - fromRow = toRow; - } - } - - //if selection is outside, move selection to last row - if (fromCol > colCount - 1) { - fromCol = colCount - 1; - selectionChanged = true; - if (toCol > fromCol) { - toCol = fromCol; - } - } else if (toCol > colCount - 1) { - toCol = colCount - 1; - selectionChanged = true; - if (fromCol > toCol) { - fromCol = toCol; - } - } - - if (selectionChanged) { - instance.selectCell(fromRow, fromCol, toRow, toCol); - } - } - }, - - /** - * Populate cells at position with 2d array - * @param {Object} start Start selection position - * @param {Array} input 2d array - * @param {Object} [end] End selection position (only for drag-down mode) - * @param {String} [source="populateFromArray"] - * @param {String} [method="overwrite"] - * @return {Object|undefined} ending td in pasted area (only if any cell was changed) - */ - populateFromArray: function (start, input, end, source, method) { - var r, rlen, c, clen, setData = [], current = {}; - rlen = input.length; - if (rlen === 0) { - return false; - } - - var repeatCol - , repeatRow - , cmax - , rmax; - - // insert data with specified pasteMode method - switch (method) { - case 'shift_down' : - repeatCol = end ? end.col - start.col + 1 : 0; - repeatRow = end ? end.row - start.row + 1 : 0; - input = Handsontable.helper.translateRowsToColumns(input); - for (c = 0, clen = input.length, cmax = Math.max(clen, repeatCol); c < cmax; c++) { - if (c < clen) { - for (r = 0, rlen = input[c].length; r < repeatRow - rlen; r++) { - input[c].push(input[c][r % rlen]); - } - input[c].unshift(start.col + c, start.row, 0); - instance.spliceCol.apply(instance, input[c]); - } - else { - input[c % clen][0] = start.col + c; - instance.spliceCol.apply(instance, input[c % clen]); - } - } - break; - - case 'shift_right' : - repeatCol = end ? end.col - start.col + 1 : 0; - repeatRow = end ? end.row - start.row + 1 : 0; - for (r = 0, rlen = input.length, rmax = Math.max(rlen, repeatRow); r < rmax; r++) { - if (r < rlen) { - for (c = 0, clen = input[r].length; c < repeatCol - clen; c++) { - input[r].push(input[r][c % clen]); - } - input[r].unshift(start.row + r, start.col, 0); - instance.spliceRow.apply(instance, input[r]); - } - else { - input[r % rlen][0] = start.row + r; - instance.spliceRow.apply(instance, input[r % rlen]); - } - } - break; - - case 'overwrite' : - default: - // overwrite and other not specified options - current.row = start.row; - current.col = start.col; - for (r = 0; r < rlen; r++) { - if ((end && current.row > end.row) || (!priv.settings.minSpareRows && current.row > instance.countRows() - 1) || (current.row >= priv.settings.maxRows)) { - break; - } - current.col = start.col; - clen = input[r] ? input[r].length : 0; - for (c = 0; c < clen; c++) { - if ((end && current.col > end.col) || (!priv.settings.minSpareCols && current.col > instance.countCols() - 1) || (current.col >= priv.settings.maxCols)) { - break; - } - if (!instance.getCellMeta(current.row, current.col).readOnly) { - setData.push([current.row, current.col, input[r][c]]); - } - current.col++; - if (end && c === clen - 1) { - c = -1; - } - } - current.row++; - if (end && r === rlen - 1) { - r = -1; - } - } - instance.setDataAtCell(setData, null, null, source || 'populateFromArray'); - break; - } - } - }; - - this.selection = selection = { //this public assignment is only temporary - inProgress: false, - - /** - * Sets inProgress to true. This enables onSelectionEnd and onSelectionEndByProp to function as desired - */ - begin: function () { - instance.selection.inProgress = true; - }, - - /** - * Sets inProgress to false. Triggers onSelectionEnd and onSelectionEndByProp - */ - finish: function () { - var sel = instance.getSelected(); - Handsontable.hooks.run(instance, "afterSelectionEnd", sel[0], sel[1], sel[2], sel[3]); - Handsontable.hooks.run(instance, "afterSelectionEndByProp", sel[0], instance.colToProp(sel[1]), sel[2], instance.colToProp(sel[3])); - instance.selection.inProgress = false; - }, - - isInProgress: function () { - return instance.selection.inProgress; - }, - - /** - * Starts selection range on given td object - * @param {WalkontableCellCoords} coords - */ - setRangeStart: function (coords) { - Handsontable.hooks.run(instance, "beforeSetRangeStart", coords); - priv.selRange = new WalkontableCellRange(coords, coords, coords); - selection.setRangeEnd(coords); - }, - - /** - * Ends selection range on given td object - * @param {WalkontableCellCoords} coords - * @param {Boolean} [scrollToCell=true] If true, viewport will be scrolled to range end - */ - setRangeEnd: function (coords, scrollToCell) { - //trigger handlers - Handsontable.hooks.run(instance, "beforeSetRangeEnd", coords); - - instance.selection.begin(); - - priv.selRange.to = coords; - if (!priv.settings.multiSelect) { - priv.selRange.from = coords; - } - - //set up current selection - instance.view.wt.selections.current.clear(); - instance.view.wt.selections.current.add(priv.selRange.highlight); - - //set up area selection - instance.view.wt.selections.area.clear(); - if (selection.isMultiple()) { - instance.view.wt.selections.area.add(priv.selRange.from); - instance.view.wt.selections.area.add(priv.selRange.to); - } - - //set up highlight - if (priv.settings.currentRowClassName || priv.settings.currentColClassName) { - instance.view.wt.selections.highlight.clear(); - instance.view.wt.selections.highlight.add(priv.selRange.from); - instance.view.wt.selections.highlight.add(priv.selRange.to); - } - - //trigger handlers - Handsontable.hooks.run(instance, "afterSelection", priv.selRange.from.row, priv.selRange.from.col, priv.selRange.to.row, priv.selRange.to.col); - Handsontable.hooks.run(instance, "afterSelectionByProp", priv.selRange.from.row, datamap.colToProp(priv.selRange.from.col), priv.selRange.to.row, datamap.colToProp(priv.selRange.to.col)); - - if (scrollToCell !== false && instance.view.mainViewIsActive()) { - instance.view.scrollViewport(coords); - } - selection.refreshBorders(); - }, - - /** - * Destroys editor, redraws borders around cells, prepares editor - * @param {Boolean} revertOriginal - * @param {Boolean} keepEditor - */ - refreshBorders: function (revertOriginal, keepEditor) { - if (!keepEditor) { - editorManager.destroyEditor(revertOriginal); - } - instance.view.render(); - if (selection.isSelected() && !keepEditor) { - editorManager.prepareEditor(); - } - }, - - /** - * Returns information if we have a multiselection - * @return {Boolean} - */ - isMultiple: function () { - return !(priv.selRange.to.col === priv.selRange.from.col && priv.selRange.to.row === priv.selRange.from.row); - }, - - /** - * Selects cell relative to current cell (if possible) - */ - transformStart: function (rowDelta, colDelta, force) { - var delta = new WalkontableCellCoords(rowDelta, colDelta); - instance.runHooks('modifyTransformStart', delta); - - if (priv.selRange.highlight.row + rowDelta > instance.countRows() - 1) { - if (force && priv.settings.minSpareRows > 0) { - instance.alter("insert_row", instance.countRows()); - } - else if (priv.settings.autoWrapCol) { - delta.row = 1 - instance.countRows(); - delta.col = priv.selRange.highlight.col + delta.col == instance.countCols() - 1 ? 1 - instance.countCols() : 1; - } - } - else if (priv.settings.autoWrapCol && priv.selRange.highlight.row + delta.row < 0 && priv.selRange.highlight.col + delta.col >= 0) { - delta.row = instance.countRows() - 1; - delta.col = priv.selRange.highlight.col + delta.col == 0 ? instance.countCols() - 1 : -1; - } - - if (priv.selRange.highlight.col + delta.col > instance.countCols() - 1) { - if (force && priv.settings.minSpareCols > 0) { - instance.alter("insert_col", instance.countCols()); - } - else if (priv.settings.autoWrapRow) { - delta.row = priv.selRange.highlight.row + delta.row == instance.countRows() - 1 ? 1 - instance.countRows() : 1; - delta.col = 1 - instance.countCols(); - } - } - else if (priv.settings.autoWrapRow && priv.selRange.highlight.col + delta.col < 0 && priv.selRange.highlight.row + delta.row >= 0) { - delta.row = priv.selRange.highlight.row + delta.row == 0 ? instance.countRows() - 1 : -1; - delta.col = instance.countCols() - 1; - } - - var totalRows = instance.countRows(); - var totalCols = instance.countCols(); - var coords = new WalkontableCellCoords(priv.selRange.highlight.row + delta.row, priv.selRange.highlight.col + delta.col); - - if (coords.row < 0) { - coords.row = 0; - } - else if (coords.row > 0 && coords.row >= totalRows) { - coords.row = totalRows - 1; - } - - if (coords.col < 0) { - coords.col = 0; - } - else if (coords.col > 0 && coords.col >= totalCols) { - coords.col = totalCols - 1; - } - - selection.setRangeStart(coords); - }, - - /** - * Sets selection end cell relative to current selection end cell (if possible) - */ - transformEnd: function (rowDelta, colDelta) { - var delta = new WalkontableCellCoords(rowDelta, colDelta); - instance.runHooks('modifyTransformEnd', delta); - - var totalRows = instance.countRows(); - var totalCols = instance.countCols(); - var coords = new WalkontableCellCoords(priv.selRange.to.row + delta.row, priv.selRange.to.col + delta.col); - - if (coords.row < 0) { - coords.row = 0; - } - else if (coords.row > 0 && coords.row >= totalRows) { - coords.row = totalRows - 1; - } - - if (coords.col < 0) { - coords.col = 0; - } - else if (coords.col > 0 && coords.col >= totalCols) { - coords.col = totalCols - 1; - } - - selection.setRangeEnd(coords); - }, - - /** - * Returns true if currently there is a selection on screen, false otherwise - * @return {Boolean} - */ - isSelected: function () { - return (priv.selRange !== null); - }, - - /** - * Returns true if coords is within current selection coords - * @param {WalkontableCellCoords} coords - * @return {Boolean} - */ - inInSelection: function (coords) { - if (!selection.isSelected()) { - return false; - } - return priv.selRange.includes(coords); - }, - - /** - * Deselects all selected cells - */ - deselect: function () { - if (!selection.isSelected()) { - return; - } - instance.selection.inProgress = false; //needed by HT inception - priv.selRange = null; - instance.view.wt.selections.current.clear(); - instance.view.wt.selections.area.clear(); - if (priv.settings.currentRowClassName || priv.settings.currentColClassName) { - instance.view.wt.selections.highlight.clear(); - } - editorManager.destroyEditor(); - selection.refreshBorders(); - Handsontable.hooks.run(instance, 'afterDeselect'); - }, - - /** - * Select all cells - */ - selectAll: function () { - if (!priv.settings.multiSelect) { - return; - } - selection.setRangeStart(new WalkontableCellCoords(0, 0)); - selection.setRangeEnd(new WalkontableCellCoords(instance.countRows() - 1, instance.countCols() - 1), false); - }, - - /** - * Deletes data from selected cells - */ - empty: function () { - if (!selection.isSelected()) { - return; - } - var topLeft = priv.selRange.getTopLeftCorner(); - var bottomRight = priv.selRange.getBottomRightCorner(); - var r, c, changes = []; - for (r = topLeft.row; r <= bottomRight.row; r++) { - for (c = topLeft.col; c <= bottomRight.col; c++) { - if (!instance.getCellMeta(r, c).readOnly) { - changes.push([r, c, '']); - } - } - } - instance.setDataAtCell(changes); - } - }; - - this.init = function () { - Handsontable.hooks.run(instance, 'beforeInit'); - - this.updateSettings(priv.settings, true); - - this.view = new Handsontable.TableView(this); - editorManager = new Handsontable.EditorManager(instance, priv, selection, datamap); - - this.forceFullRender = true; //used when data was changed - this.view.render(); - - if (typeof priv.firstRun === 'object') { - Handsontable.hooks.run(instance, 'afterChange', priv.firstRun[0], priv.firstRun[1]); - priv.firstRun = false; - } - Handsontable.hooks.run(instance, 'afterInit'); - }; - - function ValidatorsQueue() { //moved this one level up so it can be used in any function here. Probably this should be moved to a separate file - var resolved = false; - - return { - validatorsInQueue: 0, - addValidatorToQueue: function () { - this.validatorsInQueue++; - resolved = false; - }, - removeValidatorFormQueue: function () { - this.validatorsInQueue = this.validatorsInQueue - 1 < 0 ? 0 : this.validatorsInQueue - 1; - this.checkIfQueueIsEmpty(); - }, - onQueueEmpty: function () { - }, - checkIfQueueIsEmpty: function () { - if (this.validatorsInQueue == 0 && resolved == false) { - resolved = true; - this.onQueueEmpty(); - } - } - }; - } - - function validateChanges(changes, source, callback) { - var waitingForValidator = new ValidatorsQueue(); - waitingForValidator.onQueueEmpty = resolve; - - for (var i = changes.length - 1; i >= 0; i--) { - if (changes[i] === null) { - changes.splice(i, 1); - } - else { - var row = changes[i][0]; - var col = datamap.propToCol(changes[i][1]); - var logicalCol = instance.runHooksAndReturn('modifyCol', col); //column order may have changes, so we need to translate physical col index (stored in datasource) to logical (displayed to user) - var cellProperties = instance.getCellMeta(row, logicalCol); - - if (cellProperties.type === 'numeric' && typeof changes[i][3] === 'string') { - if (changes[i][3].length > 0 && /^-?[\d\s]*(\.|\,)?\d*$/.test(changes[i][3])) { - - if(typeof cellProperties.language == 'undefined') { - numeral.language('en'); - } else { - numeral.language(cellProperties.language); - } - - changes[i][3] = parseFloat(changes[i][3].replace(',','.')); - } - } - - if (instance.getCellValidator(cellProperties)) { - waitingForValidator.addValidatorToQueue(); - instance.validateCell(changes[i][3], cellProperties, (function (i, cellProperties) { - return function (result) { - if (typeof result !== 'boolean') { - throw new Error("Validation error: result is not boolean"); - } - if (result === false && cellProperties.allowInvalid === false) { - changes.splice(i, 1); // cancel the change - cellProperties.valid = true; // we cancelled the change, so cell value is still valid - --i; - } - waitingForValidator.removeValidatorFormQueue(); - } - })(i, cellProperties) - , source); - } - } - } - waitingForValidator.checkIfQueueIsEmpty(); - - function resolve() { - var beforeChangeResult; - - if (changes.length) { - beforeChangeResult = Handsontable.hooks.execute(instance, "beforeChange", changes, source); - if (typeof beforeChangeResult === 'function') { - $.when(result).then(function () { - callback(); //called when async validators and async beforeChange are resolved - }); - } - else if (beforeChangeResult === false) { - changes.splice(0, changes.length); //invalidate all changes (remove everything from array) - } - } - if (typeof beforeChangeResult !== 'function') { - callback(); //called when async validators are resolved and beforeChange was not async - } - } - } - - /** - * Internal function to apply changes. Called after validateChanges - * @param {Array} changes Array in form of [row, prop, oldValue, newValue] - * @param {String} source String that identifies how this change will be described in changes array (useful in onChange callback) - */ - function applyChanges(changes, source) { - var i = changes.length - 1; - - if (i < 0) { - return; - } - - for (; 0 <= i; i--) { - if (changes[i] === null) { - changes.splice(i, 1); - continue; - } - - if(changes[i][2] == null && changes[i][3] == null) { - continue; - } - - if (priv.settings.minSpareRows) { - while (changes[i][0] > instance.countRows() - 1) { - datamap.createRow(); - } - } - - if (instance.dataType === 'array' && priv.settings.minSpareCols) { - while (datamap.propToCol(changes[i][1]) > instance.countCols() - 1) { - datamap.createCol(); - } - } - - datamap.set(changes[i][0], changes[i][1], changes[i][3]); - } - - instance.forceFullRender = true; //used when data was changed - grid.adjustRowsAndCols(); - Handsontable.hooks.run(instance, 'beforeChangeRender', changes, source); - selection.refreshBorders(null, true); - Handsontable.hooks.run(instance, 'afterChange', changes, source || 'edit'); - } - - this.validateCell = function (value, cellProperties, callback, source) { - var validator = instance.getCellValidator(cellProperties); - - if (Object.prototype.toString.call(validator) === '[object RegExp]') { - validator = (function (validator) { - return function (value, callback) { - callback(validator.test(value)); - } - })(validator); - } - - if (typeof validator == 'function') { - - value = Handsontable.hooks.execute(instance, "beforeValidate", value, cellProperties.row, cellProperties.prop, source); - - // To provide consistent behaviour, validation should be always asynchronous - instance._registerTimeout(setTimeout(function () { - validator.call(cellProperties, value, function (valid) { - cellProperties.valid = valid; - - valid = Handsontable.hooks.execute(instance, "afterValidate", valid, value, cellProperties.row, cellProperties.prop, source); - - callback(valid); - }); - - return value; - }, 0)); - } else { //resolve callback even if validator function was not found - cellProperties.valid = true; - callback(true); - } - - - - }; - - function setDataInputToArray(row, prop_or_col, value) { - if (typeof row === "object") { //is it an array of changes - return row; - } - else if ($.isPlainObject(value)) { //backwards compatibility - return value; - } - else { - return [ - [row, prop_or_col, value] - ]; - } - } - - /** - * Set data at given cell - * @public - * @param {Number|Array} row or array of changes in format [[row, col, value], ...] - * @param {Number|String} col or source String - * @param {String} value - * @param {String} source String that identifies how this change will be described in changes array (useful in onChange callback) - */ - this.setDataAtCell = function (row, col, value, source) { - var input = setDataInputToArray(row, col, value) - , i - , ilen - , changes = [] - , prop; - - for (i = 0, ilen = input.length; i < ilen; i++) { - if (typeof input[i] !== 'object') { - throw new Error('Method `setDataAtCell` accepts row number or changes array of arrays as its first parameter'); - } - if (typeof input[i][1] !== 'number') { - throw new Error('Method `setDataAtCell` accepts row and column number as its parameters. If you want to use object property name, use method `setDataAtRowProp`'); - } - prop = datamap.colToProp(input[i][1]); - changes.push([ - input[i][0], - prop, - datamap.get(input[i][0], prop), - input[i][2] - ]); - } - - if (!source && typeof row === "object") { - source = col; - } - - validateChanges(changes, source, function () { - applyChanges(changes, source); - }); - }; - - - /** - * Set data at given row property - * @public - * @param {Number|Array} row or array of changes in format [[row, prop, value], ...] - * @param {String} prop or source String - * @param {String} value - * @param {String} source String that identifies how this change will be described in changes array (useful in onChange callback) - */ - this.setDataAtRowProp = function (row, prop, value, source) { - var input = setDataInputToArray(row, prop, value) - , i - , ilen - , changes = []; - - for (i = 0, ilen = input.length; i < ilen; i++) { - changes.push([ - input[i][0], - input[i][1], - datamap.get(input[i][0], input[i][1]), - input[i][2] - ]); - } - - if (!source && typeof row === "object") { - source = prop; - } - - validateChanges(changes, source, function () { - applyChanges(changes, source); - }); - }; - - /** - * Listen to document body keyboard input - */ - this.listen = function () { - Handsontable.activeGuid = instance.guid; - - if (document.activeElement && document.activeElement !== document.body) { - document.activeElement.blur(); - } - else if (!document.activeElement) { //IE - document.body.focus(); - } - }; - - /** - * Stop listening to document body keyboard input - */ - this.unlisten = function () { - Handsontable.activeGuid = null; - }; - - /** - * Returns true if current Handsontable instance is listening on document body keyboard input - */ - this.isListening = function () { - return Handsontable.activeGuid === instance.guid; - }; - - /** - * Destroys current editor, renders and selects current cell. If revertOriginal != true, edited data is saved - * @param {Boolean} revertOriginal - */ - this.destroyEditor = function (revertOriginal) { - selection.refreshBorders(revertOriginal); - }; - - /** - * Populate cells at position with 2d array - * @param {Number} row Start row - * @param {Number} col Start column - * @param {Array} input 2d array - * @param {Number=} endRow End row (use when you want to cut input when certain row is reached) - * @param {Number=} endCol End column (use when you want to cut input when certain column is reached) - * @param {String=} [source="populateFromArray"] - * @param {String=} [method="overwrite"] - * @return {Object|undefined} ending td in pasted area (only if any cell was changed) - */ - this.populateFromArray = function (row, col, input, endRow, endCol, source, method) { - if (!(typeof input === 'object' && typeof input[0] === 'object')) { - throw new Error("populateFromArray parameter `input` must be an array of arrays"); //API changed in 0.9-beta2, let's check if you use it correctly - } - return grid.populateFromArray(new WalkontableCellCoords(row, col), input, typeof endRow === 'number' ? new WalkontableCellCoords(endRow, endCol) : null, source, method); - }; - - /** - * Adds/removes data from the column - * @param {Number} col Index of column in which do you want to do splice. - * @param {Number} index Index at which to start changing the array. If negative, will begin that many elements from the end - * @param {Number} amount An integer indicating the number of old array elements to remove. If amount is 0, no elements are removed - * param {...*} elements Optional. The elements to add to the array. If you don't specify any elements, spliceCol simply removes elements from the array - */ - this.spliceCol = function (col, index, amount/*, elements... */) { - return datamap.spliceCol.apply(datamap, arguments); - }; - - /** - * Adds/removes data from the row - * @param {Number} row Index of column in which do you want to do splice. - * @param {Number} index Index at which to start changing the array. If negative, will begin that many elements from the end - * @param {Number} amount An integer indicating the number of old array elements to remove. If amount is 0, no elements are removed - * param {...*} elements Optional. The elements to add to the array. If you don't specify any elements, spliceCol simply removes elements from the array - */ - this.spliceRow = function (row, index, amount/*, elements... */) { - return datamap.spliceRow.apply(datamap, arguments); - }; - - /** - * Returns current selection. Returns undefined if there is no selection. - * @public - * @return {Array} [`startRow`, `startCol`, `endRow`, `endCol`] - */ - this.getSelected = function () { //https://github.com/handsontable/jquery-handsontable/issues/44 //cjl - if (selection.isSelected()) { - return [priv.selRange.from.row, priv.selRange.from.col, priv.selRange.to.row, priv.selRange.to.col]; - } - }; - - /** - * Returns current selection as a WalkontableCellRange object. Returns undefined if there is no selection. - * @public - * @return {WalkontableCellRange} - */ - this.getSelectedRange = function () { //https://github.com/handsontable/jquery-handsontable/issues/44 //cjl - if (selection.isSelected()) { - return priv.selRange; - } - }; - - - /** - * Render visible data - * @public - */ - this.render = function () { - if (instance.view) { - instance.forceFullRender = true; //used when data was changed - selection.refreshBorders(null, true); - } - }; - - /** - * Load data from array - * @public - * @param {Array} data - */ - this.loadData = function (data) { - if (typeof data === 'object' && data !== null) { - if (!(data.push && data.splice)) { //check if data is array. Must use duck-type check so Backbone Collections also pass it - //when data is not an array, attempt to make a single-row array of it - data = [data]; - } - } - else if(data === null) { - data = []; - var row; - for (var r = 0, rlen = priv.settings.startRows; r < rlen; r++) { - row = []; - for (var c = 0, clen = priv.settings.startCols; c < clen; c++) { - row.push(null); - } - data.push(row); - } - } - else { - throw new Error("loadData only accepts array of objects or array of arrays (" + typeof data + " given)"); - } - - priv.isPopulated = false; - GridSettings.prototype.data = data; - - if (priv.settings.dataSchema instanceof Array || data[0] instanceof Array) { - instance.dataType = 'array'; - } - else if (typeof priv.settings.dataSchema === 'function') { - instance.dataType = 'function'; - } - else { - instance.dataType = 'object'; - } - - datamap = new Handsontable.DataMap(instance, priv, GridSettings); - - clearCellSettingCache(); - - grid.adjustRowsAndCols(); - Handsontable.hooks.run(instance, 'afterLoadData'); - - if (priv.firstRun) { - priv.firstRun = [null, 'loadData']; - } - else { - Handsontable.hooks.run(instance, 'afterChange', null, 'loadData'); - instance.render(); - } - - priv.isPopulated = true; - - - - function clearCellSettingCache() { - priv.cellSettings.length = 0; - } - }; - - /** - * Return the current data object (the same that was passed by `data` configuration option or `loadData` method). Optionally you can provide cell range `r`, `c`, `r2`, `c2` to get only a fragment of grid data - * @public - * @param {Number} r (Optional) From row - * @param {Number} c (Optional) From col - * @param {Number} r2 (Optional) To row - * @param {Number} c2 (Optional) To col - * @return {Array|Object} - */ - this.getData = function (r, c, r2, c2) { - if (typeof r === 'undefined') { - return datamap.getAll(); - } else { - return datamap.getRange(new WalkontableCellCoords(r, c), new WalkontableCellCoords(r2, c2), datamap.DESTINATION_RENDERER); - } - }; - - this.getCopyableData = function (startRow, startCol, endRow, endCol) { - return datamap.getCopyableText(new WalkontableCellCoords(startRow, startCol), new WalkontableCellCoords(endRow, endCol)); - }; - - /** - * Update settings - * @public - */ - this.updateSettings = function (settings, init) { - var i, clen; - - if (typeof settings.rows !== "undefined") { - throw new Error("'rows' setting is no longer supported. do you mean startRows, minRows or maxRows?"); - } - if (typeof settings.cols !== "undefined") { - throw new Error("'cols' setting is no longer supported. do you mean startCols, minCols or maxCols?"); - } - - for (i in settings) { - if (i === 'data') { - continue; //loadData will be triggered later - } - else { - if (Handsontable.hooks.hooks[i] !== void 0 || Handsontable.hooks.legacy[i] !== void 0) { - if (typeof settings[i] === 'function' || Handsontable.helper.isArray(settings[i])) { - instance.addHook(i, settings[i]); - } - } - else { - // Update settings - if (!init && settings.hasOwnProperty(i)) { - GridSettings.prototype[i] = settings[i]; - } - } - } - } - - // Load data or create data map - if (settings.data === void 0 && priv.settings.data === void 0) { - instance.loadData(null); //data source created just now - } - else if (settings.data !== void 0) { - instance.loadData(settings.data); //data source given as option - } - else if (settings.columns !== void 0) { - datamap.createMap(); - } - - // Init columns constructors configuration - clen = instance.countCols(); - - //Clear cellSettings cache - priv.cellSettings.length = 0; - - if (clen > 0) { - var proto, column; - - for (i = 0; i < clen; i++) { - priv.columnSettings[i] = Handsontable.helper.columnFactory(GridSettings, priv.columnsSettingConflicts); - - // shortcut for prototype - proto = priv.columnSettings[i].prototype; - - // Use settings provided by user - if (GridSettings.prototype.columns) { - column = GridSettings.prototype.columns[i]; - Handsontable.helper.extend(proto, column); - Handsontable.helper.extend(proto, expandType(column)); - } - } - } - - if (typeof settings.cell !== 'undefined') { - for(i in settings.cell) { - var cell = settings.cell[i]; - instance.setCellMetaObject(cell.row, cell.col, cell); - } - } - - Handsontable.hooks.run(instance, 'afterCellMetaReset'); - - if (typeof settings.className !== "undefined") { - if (GridSettings.prototype.className) { - instance.rootElement.removeClass(GridSettings.prototype.className); - } - if (settings.className) { - instance.rootElement.addClass(settings.className); - } - } - - if (typeof settings.height != 'undefined'){ - var height = settings.height; - - if (typeof height == 'function'){ - height = height(); - } - - instance.rootElement[0].style.height = height + 'px'; - } - - if (typeof settings.width != 'undefined'){ - var width = settings.width; - - if (typeof width == 'function'){ - width = width(); - } - - instance.rootElement[0].style.width = width + 'px'; - } - - if (height){ - instance.rootElement[0].style.overflow = 'auto'; - } - - if (!init) { - Handsontable.hooks.run(instance, 'afterUpdateSettings'); - } - - grid.adjustRowsAndCols(); - if (instance.view && !priv.firstRun) { - instance.forceFullRender = true; //used when data was changed - selection.refreshBorders(null, true); - } - }; - - this.getValue = function () { - var sel = instance.getSelected(); - if (GridSettings.prototype.getValue) { - if (typeof GridSettings.prototype.getValue === 'function') { - return GridSettings.prototype.getValue.call(instance); - } - else if (sel) { - return instance.getData()[sel[0]][GridSettings.prototype.getValue]; - } - } - else if (sel) { - return instance.getDataAtCell(sel[0], sel[1]); - } - }; - - function expandType(obj) { - if (!obj.hasOwnProperty('type')) return; //ignore obj.prototype.type - - - var type, expandedType = {}; - - if (typeof obj.type === 'object') { - type = obj.type; - } - else if (typeof obj.type === 'string') { - type = Handsontable.cellTypes[obj.type]; - if (type === void 0) { - throw new Error('You declared cell type "' + obj.type + '" as a string that is not mapped to a known object. Cell type must be an object or a string mapped to an object in Handsontable.cellTypes'); - } - } - - - for (var i in type) { - if (type.hasOwnProperty(i) && !obj.hasOwnProperty(i)) { - expandedType[i] = type[i]; - } - } - - return expandedType; - - }; - - /** - * Returns current settings object - * @return {Object} - */ - this.getSettings = function () { - return priv.settings; - }; - - /** - * Clears grid - * @public - */ - this.clear = function () { - selection.selectAll(); - selection.empty(); - }; - - /** - * Inserts or removes rows and columns - * @param {String} action See grid.alter for possible values - * @param {Number} index - * @param {Number} amount - * @param {String} [source] Optional. Source of hook runner. - * @param {Boolean} [keepEmptyRows] Optional. Flag for preventing deletion of empty rows. - * @public - */ - this.alter = function (action, index, amount, source, keepEmptyRows) { - grid.alter(action, index, amount, source, keepEmptyRows); - }; - - /** - * Returns
  • '; - - var CAPTION = document.createElement('CAPTION'); - CAPTION.innerHTML = 'c
    c
    c
    c'; - CAPTION.style.padding = 0; - CAPTION.style.margin = 0; - TABLE.insertBefore(CAPTION, TBODY); - - document.body.appendChild(TABLE); - hasCaptionProblem = (TABLE.offsetHeight < 2 * TABLE.lastChild.offsetHeight); //boolean - document.body.removeChild(TABLE); - } - - Handsontable.Dom.hasCaptionProblem = function () { - if (hasCaptionProblem === void 0) { - detectCaptionProblem(); - } - return hasCaptionProblem; - }; - - /** - * Returns caret position in text input - * @author http://stackoverflow.com/questions/263743/how-to-get-caret-position-in-textarea - * @return {Number} - */ - Handsontable.Dom.getCaretPosition = function (el) { - if (el.selectionStart) { - return el.selectionStart; - } - else if (document.selection) { //IE8 - el.focus(); - var r = document.selection.createRange(); - if (r == null) { - return 0; - } - var re = el.createTextRange(), - rc = re.duplicate(); - re.moveToBookmark(r.getBookmark()); - rc.setEndPoint('EndToStart', re); - return rc.text.length; - } - return 0; - }; - - /** - * Sets caret position in text input - * @author http://blog.vishalon.net/index.php/javascript-getting-and-setting-caret-position-in-textarea/ - * @param {Element} el - * @param {Number} pos - * @param {Number} endPos - */ - Handsontable.Dom.setCaretPosition = function (el, pos, endPos) { - if (endPos === void 0) { - endPos = pos; - } - if (el.setSelectionRange) { - el.focus(); - el.setSelectionRange(pos, endPos); - } - else if (el.createTextRange) { //IE8 - var range = el.createTextRange(); - range.collapse(true); - range.moveEnd('character', endPos); - range.moveStart('character', pos); - range.select(); - } - }; - - var cachedScrollbarWidth; - //http://stackoverflow.com/questions/986937/how-can-i-get-the-browsers-scrollbar-sizes - function walkontableCalculateScrollbarWidth() { - var inner = document.createElement('p'); - inner.style.width = "100%"; - inner.style.height = "200px"; - - var outer = document.createElement('div'); - outer.style.position = "absolute"; - outer.style.top = "0px"; - outer.style.left = "0px"; - outer.style.visibility = "hidden"; - outer.style.width = "200px"; - outer.style.height = "150px"; - outer.style.overflow = "hidden"; - outer.appendChild(inner); - - (document.body || document.documentElement).appendChild(outer); - var w1 = inner.offsetWidth; - outer.style.overflow = 'scroll'; - var w2 = inner.offsetWidth; - if (w1 == w2) w2 = outer.clientWidth; - - (document.body || document.documentElement).removeChild(outer); - - return (w1 - w2); - } - - /** - * Returns the computed width of the native browser scroll bar - * @return {Number} width - */ - Handsontable.Dom.getScrollbarWidth = function () { - if (cachedScrollbarWidth === void 0) { - cachedScrollbarWidth = walkontableCalculateScrollbarWidth(); - } - return cachedScrollbarWidth; - } -})(); - -/** - * Handsontable TableView constructor - * @param {Object} instance - */ -Handsontable.TableView = function (instance) { - var that = this - , $window = $(window) - , $documentElement = $(document.documentElement); - - this.instance = instance; - this.settings = instance.getSettings(); - - instance.rootElement.data('originalStyle', instance.rootElement[0].getAttribute('style')); //needed to retrieve original style in jsFiddle link generator in HT examples. may be removed in future versions - // in IE7 getAttribute('style') returns an object instead of a string, but we only support IE8+ - - instance.rootElement.addClass('handsontable'); - - var table = document.createElement('TABLE'); - table.className = 'htCore'; - this.THEAD = document.createElement('THEAD'); - table.appendChild(this.THEAD); - this.TBODY = document.createElement('TBODY'); - table.appendChild(this.TBODY); - - instance.$table = $(table); - instance.container.prepend(instance.$table); - - instance.rootElement.on('mousedown.handsontable', function (event) { - if (!that.isTextSelectionAllowed(event.target)) { - clearTextSelection(); - event.preventDefault(); - window.focus(); //make sure that window that contains HOT is active. Important when HOT is in iframe. - } - }); - - $documentElement.on('keyup.' + instance.guid, function (event) { - if (instance.selection.isInProgress() && !event.shiftKey) { - instance.selection.finish(); - } - }); - - var isMouseDown; - this.isMouseDown = function() { - return isMouseDown; - } - - $documentElement.on('mouseup.' + instance.guid, function (event) { - if (instance.selection.isInProgress() && event.which === 1) { //is left mouse button - instance.selection.finish(); - } - - isMouseDown = false; - - if (Handsontable.helper.isOutsideInput(document.activeElement)) { - instance.unlisten(); - } - }); - - $documentElement.on('mousedown.' + instance.guid, function (event) { - var next = event.target; - - if (next.shadowRoot) { - return; //click inside Web Component - } - - if (next !== that.wt.wtTable.spreader) { //immediate click on "spreader" means click on the right side of vertical scrollbar - while (next !== document.documentElement) { - if (next === null) { - return; //click on something that was a row but now is detached (possibly because your click triggered a rerender) - } - if (next === instance.rootElement[0]) { - return; //click inside container - } - next = next.parentNode; - } - } - - //function did not return until here, we have an outside click! - - if (that.settings.outsideClickDeselects) { - instance.deselectCell(); - } - else { - instance.destroyEditor(); - } - }); - - instance.$table.on('selectstart', function (event) { - if (that.settings.fragmentSelection) { - return; - } - - //https://github.com/handsontable/jquery-handsontable/issues/160 - //selectstart is IE only event. Prevent text from being selected when performing drag down in IE8 - event.preventDefault(); - }); - - var clearTextSelection = function () { - //http://stackoverflow.com/questions/3169786/clear-text-selection-with-javascript - if (window.getSelection) { - if (window.getSelection().empty) { // Chrome - window.getSelection().empty(); - } else if (window.getSelection().removeAllRanges) { // Firefox - window.getSelection().removeAllRanges(); - } - } else if (document.selection) { // IE? - document.selection.empty(); - } - }; - - var walkontableConfig = { - debug: function () { - return that.settings.debug; - }, - table: table, - stretchH: this.settings.stretchH, - data: instance.getDataAtCell, - totalRows: instance.countRows, - totalColumns: instance.countCols, - offsetRow: 0, - fixedColumnsLeft: function () { - return that.settings.fixedColumnsLeft; - }, - fixedRowsTop: function () { - return that.settings.fixedRowsTop; - }, - renderAllRows: that.settings.renderAllRows, - rowHeaders: function () { - return instance.hasRowHeaders() ? [function (index, TH) { - that.appendRowHeader(index, TH); - }] : [] - }, - columnHeaders: function () { - return instance.hasColHeaders() ? [function (index, TH) { - that.appendColHeader(index, TH); - }] : [] - }, - columnWidth: instance.getColWidth, - rowHeight: instance.getRowHeight, - cellRenderer: function (row, col, TD) { - - var prop = that.instance.colToProp(col) - , cellProperties = that.instance.getCellMeta(row, col) - , renderer = that.instance.getCellRenderer(cellProperties); - - var value = that.instance.getDataAtRowProp(row, prop); - - renderer(that.instance, TD, row, col, prop, value, cellProperties); - Handsontable.hooks.run(that.instance, 'afterRenderer', TD, row, col, prop, value, cellProperties); - - }, - selections: [ - { - className: 'current', - border: { - width: 2, - color: '#5292F7', - //style: 'solid', //not used - cornerVisible: function () { - return that.settings.fillHandle && !that.isCellEdited() && !instance.selection.isMultiple() - } - } - }, - { - className: 'area', - border: { - width: 1, - color: '#89AFF9', - //style: 'solid', // not used - cornerVisible: function () { - return that.settings.fillHandle && !that.isCellEdited() && instance.selection.isMultiple() - } - } - }, - { - className: 'highlight', - highlightRowClassName: that.settings.currentRowClassName, - highlightColumnClassName: that.settings.currentColClassName - }, - { - className: 'fill', - border: { - width: 1, - color: 'red' - //style: 'solid' // not used - } - } - ], - hideBorderOnMouseDownOver: function () { - return that.settings.fragmentSelection; - }, - onCellMouseDown: function (event, coords, TD, wt) { - instance.listen(); - that.activeWt = wt; - - isMouseDown = true; - - if (event.button === 2 && instance.selection.inInSelection(coords)) { //right mouse button - //do nothing - } - else if (event.shiftKey) { - if (coords.row >= 0 && coords.col >= 0) { - instance.selection.setRangeEnd(coords); - } - } - else { - if (coords.row < 0 || coords.col < 0) { - if (coords.row < 0) { - instance.selectCell(0, coords.col, instance.countRows() - 1, coords.col); - } - if (coords.col < 0) { - instance.selectCell(coords.row, 0, coords.row, instance.countCols() - 1); - } - } - else { - instance.selection.setRangeStart(coords); - } - } - - Handsontable.hooks.run(instance, 'afterOnCellMouseDown', event, coords, TD); - - that.activeWt = that.wt; - }, - /*onCellMouseOut: function (/*event, coords, TD* /) { - if (isMouseDown && that.settings.fragmentSelection === 'single') { - clearTextSelection(); //otherwise text selection blinks during multiple cells selection - } - },*/ - onCellMouseOver: function (event, coords, TD, wt) { - that.activeWt = wt; - if (coords.row >= 0 && coords.col >= 0) { //is not a header - if (isMouseDown) { - /*if (that.settings.fragmentSelection === 'single') { - clearTextSelection(); //otherwise text selection blinks during multiple cells selection - }*/ - instance.selection.setRangeEnd(coords); - } - } - Handsontable.hooks.run(instance, 'afterOnCellMouseOver', event, coords, TD); - that.activeWt = that.wt; - }, - onCellCornerMouseDown: function (event) { - event.preventDefault(); - Handsontable.hooks.run(instance, 'afterOnCellCornerMouseDown', event); - }, - beforeDraw: function (force) { - that.beforeRender(force); - }, - onDraw: function(force){ - that.onDraw(force); - }, - onScrollVertically: function () { - instance.runHooks('afterScrollVertically'); - }, - onScrollHorizontally: function () { - instance.runHooks('afterScrollHorizontally'); - } - }; - - Handsontable.hooks.run(instance, 'beforeInitWalkontable', walkontableConfig); - - this.wt = new Walkontable(walkontableConfig); - this.activeWt = this.wt; - - $(that.wt.wtTable.spreader).on('mousedown.handsontable, contextmenu.handsontable', function (event) { - if (event.target === that.wt.wtTable.spreader && event.which === 3) { //right mouse button exactly on spreader means right clickon the right hand side of vertical scrollbar - event.stopPropagation(); - } - }); - - $documentElement.on('click.' + instance.guid, function () { - if (that.settings.observeDOMVisibility) { - if (that.wt.drawInterrupted) { - that.instance.forceFullRender = true; - that.render(); - } - } - }); -}; - -Handsontable.TableView.prototype.isTextSelectionAllowed = function (el) { - if ( Handsontable.helper.isInput(el) ) { - return (true); - } - if (this.settings.fragmentSelection && Handsontable.Dom.isChildOf(el, this.TBODY)) { - return (true); - } - return false; -}; - -Handsontable.TableView.prototype.isCellEdited = function () { - var activeEditor = this.instance.getActiveEditor(); - return activeEditor && activeEditor.isOpened(); -}; - -Handsontable.TableView.prototype.getWidth = function () { - return this.wt.wtViewport.getWorkspaceActualWidth(); -}; - -Handsontable.TableView.prototype.getHeight = function () { - return this.wt.wtViewport.getWorkspaceActualHeight(); -}; - -Handsontable.TableView.prototype.beforeRender = function (force) { - if (force) { //force = did Walkontable decide to do full render - Handsontable.hooks.run(this.instance, 'beforeRender', this.instance.forceFullRender); //this.instance.forceFullRender = did Handsontable request full render? - } -}; - -Handsontable.TableView.prototype.onDraw = function(force){ - if (force) { //force = did Walkontable decide to do full render - Handsontable.hooks.run(this.instance, 'afterRender', this.instance.forceFullRender); //this.instance.forceFullRender = did Handsontable request full render? - } -}; - -Handsontable.TableView.prototype.render = function () { - this.wt.draw(!this.instance.forceFullRender); - this.instance.forceFullRender = false; - this.instance.rootElement.triggerHandler('render.handsontable'); -}; - -/** - * Returns td object given coordinates - * @param {WalkontableCellCoords} coords - */ -Handsontable.TableView.prototype.getCellAtCoords = function (coords) { - var td = this.wt.wtTable.getCell(coords); - if (td < 0) { //there was an exit code (cell is out of bounds) - return null; - } - else { - return td; - } -}; - -/** - * Scroll viewport to selection - * @param {WalkontableCellCoords} coords - */ -Handsontable.TableView.prototype.scrollViewport = function (coords) { - this.wt.scrollViewport(coords); -}; - -/** - * Append row header to a TH element - * @param row - * @param TH - */ -Handsontable.TableView.prototype.appendRowHeader = function (row, TH) { - var DIV = document.createElement('DIV'), - SPAN = document.createElement('SPAN'); - - DIV.className = 'relative'; - SPAN.className = 'rowHeader'; - - if (row > -1) { - Handsontable.Dom.fastInnerHTML(SPAN, this.instance.getRowHeader(row)); - } else { - Handsontable.Dom.fastInnerText(SPAN, '\u00A0'); - } - - DIV.appendChild(SPAN); - Handsontable.Dom.empty(TH); - - TH.appendChild(DIV); - - Handsontable.hooks.run(this.instance, 'afterGetRowHeader', row, TH); -}; - -/** - * Append column header to a TH element - * @param col - * @param TH - */ -Handsontable.TableView.prototype.appendColHeader = function (col, TH) { - var DIV = document.createElement('DIV') - , SPAN = document.createElement('SPAN'); - - DIV.className = 'relative'; - SPAN.className = 'colHeader'; - - Handsontable.Dom.fastInnerHTML(SPAN, this.instance.getColHeader(col)); - DIV.appendChild(SPAN); - - Handsontable.Dom.empty(TH); - TH.appendChild(DIV); - Handsontable.hooks.run(this.instance, 'afterGetColHeader', col, TH); -}; - -/** - * Given a element's left position relative to the viewport, returns maximum element width until the right edge of the viewport (before scrollbar) - * @param {Number} left - * @return {Number} - */ -Handsontable.TableView.prototype.maximumVisibleElementWidth = function (leftOffset) { - this.wt.wtScrollbars.horizontal.readWindowSize(); - var workspaceWidth = this.wt.wtViewport.getWorkspaceWidth(); - var maxWidth = workspaceWidth - leftOffset; - return maxWidth > 0 ? maxWidth : 0; -}; - -/** - * Given a element's top position relative to the viewport, returns maximum element height until the bottom edge of the viewport (before scrollbar) - * @param {Number} topOffset - * @return {Number} - */ -Handsontable.TableView.prototype.maximumVisibleElementHeight = function (topOffset) { - this.wt.wtScrollbars.vertical.readWindowSize(); - var workspaceHeight = this.wt.wtViewport.getWorkspaceHeight(); - var maxHeight = workspaceHeight - topOffset; - return maxHeight > 0 ? maxHeight : 0; -}; - -Handsontable.TableView.prototype.mainViewIsActive = function () { - return this.wt === this.activeWt; -}; - -/** - * Utility to register editors and common namespace for keeping reference to all editor classes - */ -(function (Handsontable) { - 'use strict'; - - function RegisteredEditor(editorClass) { - var clazz, instances; - - instances = {}; - clazz = editorClass; - - this.getInstance = function (hotInstance) { - if (!(hotInstance.guid in instances)) { - instances[hotInstance.guid] = new clazz(hotInstance); - } - - return instances[hotInstance.guid]; - } - - } - - var registeredEditorNames = {}; - var registeredEditorClasses = new WeakMap(); - - Handsontable.editors = { - - /** - * Registers editor under given name - * @param {String} editorName - * @param {Function} editorClass - */ - registerEditor: function (editorName, editorClass) { - var editor = new RegisteredEditor(editorClass); - if (typeof editorName === "string") { - registeredEditorNames[editorName] = editor; - } - registeredEditorClasses.set(editorClass, editor); - }, - - /** - * Returns instance (singleton) of editor class - * @param {String|Function} editorName/editorClass - * @returns {Function} editorClass - */ - getEditor: function (editorName, hotInstance) { - var editor; - if (typeof editorName == 'function') { - if (!(registeredEditorClasses.get(editorName))) { - this.registerEditor(null, editorName); - } - editor = registeredEditorClasses.get(editorName); - } - else if (typeof editorName == 'string') { - editor = registeredEditorNames[editorName]; - } - else { - throw Error('Only strings and functions can be passed as "editor" parameter '); - } - - if (!editor) { - throw Error('No editor registered under name "' + editorName + '"'); - } - - return editor.getInstance(hotInstance); - } - - }; - - -})(Handsontable); - -(function(Handsontable){ - 'use strict'; - - Handsontable.EditorManager = function(instance, priv, selection){ - var that = this; - var $document = $(document); - var keyCodes = Handsontable.helper.keyCode; - var destroyed = false; - - var activeEditor; - - var init = function () { - - function onKeyDown(event) { - - if (!instance.isListening()) { - return; - } - - if (priv.settings.beforeOnKeyDown) { // HOT in HOT Plugin - priv.settings.beforeOnKeyDown.call(instance, event); - } - - Handsontable.hooks.run(instance, 'beforeKeyDown', event); - - if(destroyed) { - return; - } - - if (!event.isImmediatePropagationStopped()) { - - priv.lastKeyCode = event.keyCode; - if (selection.isSelected()) { - var ctrlDown = (event.ctrlKey || event.metaKey) && !event.altKey; //catch CTRL but not right ALT (which in some systems triggers ALT+CTRL) - - if (!activeEditor.isWaiting()) { - if (!Handsontable.helper.isMetaKey(event.keyCode) && !ctrlDown && !that.isEditorOpened()) { - that.openEditor(''); - event.stopPropagation(); //required by HandsontableEditor - return; - } - } - - var rangeModifier = event.shiftKey ? selection.setRangeEnd : selection.setRangeStart; - - switch (event.keyCode) { - - case keyCodes.A: - if (ctrlDown) { - selection.selectAll(); //select all cells - - event.preventDefault(); - event.stopPropagation(); - break; - } - - case keyCodes.ARROW_UP: - - if (that.isEditorOpened() && !activeEditor.isWaiting()){ - that.closeEditorAndSaveChanges(ctrlDown); - } - - moveSelectionUp(event.shiftKey); - - event.preventDefault(); - event.stopPropagation(); //required by HandsontableEditor - break; - - case keyCodes.ARROW_DOWN: - if (that.isEditorOpened() && !activeEditor.isWaiting()){ - that.closeEditorAndSaveChanges(ctrlDown); - } - - moveSelectionDown(event.shiftKey); - - event.preventDefault(); - event.stopPropagation(); //required by HandsontableEditor - break; - - case keyCodes.ARROW_RIGHT: - if(that.isEditorOpened() && !activeEditor.isWaiting()){ - that.closeEditorAndSaveChanges(ctrlDown); - } - - moveSelectionRight(event.shiftKey); - - event.preventDefault(); - event.stopPropagation(); //required by HandsontableEditor - break; - - case keyCodes.ARROW_LEFT: - if(that.isEditorOpened() && !activeEditor.isWaiting()){ - that.closeEditorAndSaveChanges(ctrlDown); - } - - moveSelectionLeft(event.shiftKey); - - event.preventDefault(); - event.stopPropagation(); //required by HandsontableEditor - break; - - case keyCodes.TAB: - var tabMoves = typeof priv.settings.tabMoves === 'function' ? priv.settings.tabMoves(event) : priv.settings.tabMoves; - if (event.shiftKey) { - selection.transformStart(-tabMoves.row, -tabMoves.col); //move selection left - } - else { - selection.transformStart(tabMoves.row, tabMoves.col, true); //move selection right (add a new column if needed) - } - event.preventDefault(); - event.stopPropagation(); //required by HandsontableEditor - break; - - case keyCodes.BACKSPACE: - case keyCodes.DELETE: - selection.empty(event); - that.prepareEditor(); - event.preventDefault(); - break; - - case keyCodes.F2: /* F2 */ - that.openEditor(); - event.preventDefault(); //prevent Opera from opening Go to Page dialog - break; - - case keyCodes.ENTER: /* return/enter */ - if(that.isEditorOpened()){ - - if (activeEditor.state !== Handsontable.EditorState.WAITING){ - that.closeEditorAndSaveChanges(ctrlDown); - } - - moveSelectionAfterEnter(event.shiftKey); - - } else { - - if (instance.getSettings().enterBeginsEditing){ - that.openEditor(); - } else { - moveSelectionAfterEnter(event.shiftKey); - } - - } - - event.preventDefault(); //don't add newline to field - event.stopImmediatePropagation(); //required by HandsontableEditor - break; - - case keyCodes.ESCAPE: - if(that.isEditorOpened()){ - that.closeEditorAndRestoreOriginalValue(ctrlDown); - } - event.preventDefault(); - break; - - case keyCodes.HOME: - if (event.ctrlKey || event.metaKey) { - rangeModifier(new WalkontableCellCoords(0, priv.selRange.from.col)); - } - else { - rangeModifier(new WalkontableCellCoords(priv.selRange.from.row, 0)); - } - event.preventDefault(); //don't scroll the window - event.stopPropagation(); //required by HandsontableEditor - break; - - case keyCodes.END: - if (event.ctrlKey || event.metaKey) { - rangeModifier(new WalkontableCellCoords(instance.countRows() - 1, priv.selRange.from.col)); - } - else { - rangeModifier(new WalkontableCellCoords(priv.selRange.from.row, instance.countCols() - 1)); - } - event.preventDefault(); //don't scroll the window - event.stopPropagation(); //required by HandsontableEditor - break; - - case keyCodes.PAGE_UP: - selection.transformStart(-instance.countVisibleRows(), 0); - instance.view.wt.scrollVertical(-instance.countVisibleRows()); - instance.view.render(); - event.preventDefault(); //don't page up the window - event.stopPropagation(); //required by HandsontableEditor - break; - - case keyCodes.PAGE_DOWN: - selection.transformStart(instance.countVisibleRows(), 0); - instance.view.wt.scrollVertical(instance.countVisibleRows()); - instance.view.render(); - event.preventDefault(); //don't page down the window - event.stopPropagation(); //required by HandsontableEditor - break; - - default: - break; - } - - } - } - } - - instance.addHook('afterDocumentKeyDown', function(originalEvent){ - onKeyDown(originalEvent); - }); - - $document.on('keydown.' + instance.guid, function(ev) { - instance.runHooks('afterDocumentKeyDown', ev); - }); - - function onDblClick(event, coords, elem) { - if(elem.nodeName == "TD") { //may be TD or TH - //that.instance.destroyEditor(); - that.openEditor(); - } - } - - instance.view.wt.update('onCellDblClick', onDblClick); - - instance.addHook('afterDestroy', function(){ - destroyed = true; - }); - - function moveSelectionAfterEnter(shiftKey){ - var enterMoves = typeof priv.settings.enterMoves === 'function' ? priv.settings.enterMoves(event) : priv.settings.enterMoves; - - if (shiftKey) { - selection.transformStart(-enterMoves.row, -enterMoves.col); //move selection up - } - else { - selection.transformStart(enterMoves.row, enterMoves.col, true); //move selection down (add a new row if needed) - } - } - - function moveSelectionUp(shiftKey){ - if (shiftKey) { - selection.transformEnd(-1, 0); - } - else { - selection.transformStart(-1, 0); - } - } - - function moveSelectionDown(shiftKey){ - if (shiftKey) { - selection.transformEnd(1, 0); //expanding selection down with shift - } - else { - selection.transformStart(1, 0); //move selection down - } - } - - function moveSelectionRight(shiftKey){ - if (shiftKey) { - selection.transformEnd(0, 1); - } - else { - selection.transformStart(0, 1); - } - } - - function moveSelectionLeft(shiftKey){ - if (shiftKey) { - selection.transformEnd(0, -1); - } - else { - selection.transformStart(0, -1); - } - } - }; - - /** - * Destroy current editor, if exists - * @param {Boolean} revertOriginal - */ - this.destroyEditor = function (revertOriginal) { - this.closeEditor(revertOriginal); - }; - - this.getActiveEditor = function () { - return activeEditor; - }; - - /** - * Prepare text input to be displayed at given grid cell - */ - this.prepareEditor = function () { - - if (activeEditor && activeEditor.isWaiting()){ - - this.closeEditor(false, false, function(dataSaved){ - if(dataSaved){ - that.prepareEditor(); - } - }); - - return; - } - - var row = priv.selRange.highlight.row; - var col = priv.selRange.highlight.col; - var prop = instance.colToProp(col); - var td = instance.getCell(row, col); - var originalValue = instance.getDataAtCell(row, col); - var cellProperties = instance.getCellMeta(row, col); - - var editorClass = instance.getCellEditor(cellProperties); - activeEditor = Handsontable.editors.getEditor(editorClass, instance); - - activeEditor.prepare(row, col, prop, td, originalValue, cellProperties); - - }; - - this.isEditorOpened = function () { - return activeEditor.isOpened(); - }; - - this.openEditor = function (initialValue) { - if (!activeEditor.cellProperties.readOnly){ - activeEditor.beginEditing(initialValue); - } - }; - - this.closeEditor = function (restoreOriginalValue, ctrlDown, callback) { - - if (!activeEditor){ - if(callback) { - callback(false); - } - } - else { - activeEditor.finishEditing(restoreOriginalValue, ctrlDown, callback); - } - }; - - this.closeEditorAndSaveChanges = function(ctrlDown){ - return this.closeEditor(false, ctrlDown); - }; - - this.closeEditorAndRestoreOriginalValue = function(ctrlDown){ - return this.closeEditor(true, ctrlDown); - }; - - init(); - }; - -})(Handsontable); - -/** - * Utility to register renderers and common namespace for keeping reference to all renderers classes - */ -(function (Handsontable) { - 'use strict'; - - var registeredRenderers = {}; - - Handsontable.renderers = { - - /** - * Registers renderer under given name - * @param {String} rendererName - * @param {Function} rendererFunction - */ - registerRenderer: function (rendererName, rendererFunction) { - registeredRenderers[rendererName] = rendererFunction - }, - - /** - * @param {String|Function} rendererName/rendererFunction - * @returns {Function} rendererFunction - */ - getRenderer: function (rendererName) { - if (typeof rendererName == 'function'){ - return rendererName; - } - - if (typeof rendererName != 'string'){ - throw Error('Only strings and functions can be passed as "renderer" parameter '); - } - - if (!(rendererName in registeredRenderers)) { - throw Error('No editor registered under name "' + rendererName + '"'); - } - - return registeredRenderers[rendererName]; - } - - }; - - -})(Handsontable); - -/** - * Returns true if keyCode represents a printable character - * @param {Number} keyCode - * @return {Boolean} - */ -Handsontable.helper.isPrintableChar = function (keyCode) { - return ((keyCode == 32) || //space - (keyCode >= 48 && keyCode <= 57) || //0-9 - (keyCode >= 96 && keyCode <= 111) || //numpad - (keyCode >= 186 && keyCode <= 192) || //;=,-./` - (keyCode >= 219 && keyCode <= 222) || //[]{}\|"' - keyCode >= 226 || //special chars (229 for Asian chars) - (keyCode >= 65 && keyCode <= 90)); //a-z -}; - -Handsontable.helper.isMetaKey = function (keyCode) { - var keyCodes = Handsontable.helper.keyCode; - var metaKeys = [ - keyCodes.ARROW_DOWN, - keyCodes.ARROW_UP, - keyCodes.ARROW_LEFT, - keyCodes.ARROW_RIGHT, - keyCodes.HOME, - keyCodes.END, - keyCodes.DELETE, - keyCodes.BACKSPACE, - keyCodes.F1, - keyCodes.F2, - keyCodes.F3, - keyCodes.F4, - keyCodes.F5, - keyCodes.F6, - keyCodes.F7, - keyCodes.F8, - keyCodes.F9, - keyCodes.F10, - keyCodes.F11, - keyCodes.F12, - keyCodes.TAB, - keyCodes.PAGE_DOWN, - keyCodes.PAGE_UP, - keyCodes.ENTER, - keyCodes.ESCAPE, - keyCodes.SHIFT, - keyCodes.CAPS_LOCK, - keyCodes.ALT - ]; - - return metaKeys.indexOf(keyCode) != -1; -}; - -Handsontable.helper.isCtrlKey = function (keyCode) { - - var keys = Handsontable.helper.keyCode; - - return [keys.CONTROL_LEFT, 224, keys.COMMAND_LEFT, keys.COMMAND_RIGHT].indexOf(keyCode) != -1; -}; - -/** - * Converts a value to string - * @param value - * @return {String} - */ -Handsontable.helper.stringify = function (value) { - switch (typeof value) { - case 'string': - case 'number': - return value + ''; - break; - - case 'object': - if (value === null) { - return ''; - } - else { - return value.toString(); - } - break; - - case 'undefined': - return ''; - break; - - default: - return value.toString(); - } -}; - -/** - * Generates spreadsheet-like column names: A, B, C, ..., Z, AA, AB, etc - * @param index - * @returns {String} - */ -Handsontable.helper.spreadsheetColumnLabel = function (index) { - var dividend = index + 1; - var columnLabel = ''; - var modulo; - while (dividend > 0) { - modulo = (dividend - 1) % 26; - columnLabel = String.fromCharCode(65 + modulo) + columnLabel; - dividend = parseInt((dividend - modulo) / 26, 10); - } - return columnLabel; -}; - -/** - * Checks if value of n is a numeric one - * http://jsperf.com/isnan-vs-isnumeric/4 - * @param n - * @returns {boolean} - */ -Handsontable.helper.isNumeric = function (n) { - var t = typeof n; - return t == 'number' ? !isNaN(n) && isFinite(n) : - t == 'string' ? !n.length ? false : - n.length == 1 ? /\d/.test(n) : - /^\s*[+-]?\s*(?:(?:\d+(?:\.\d+)?(?:e[+-]?\d+)?)|(?:0x[a-f\d]+))\s*$/i.test(n) : - t == 'object' ? !!n && typeof n.valueOf() == "number" && !(n instanceof Date) : false; -}; - -/** - * Checks if child is a descendant of given parent node - * http://stackoverflow.com/questions/2234979/how-to-check-in-javascript-if-one-element-is-a-child-of-another - * @param parent - * @param child - * @returns {boolean} - */ -Handsontable.helper.isDescendant = function (parent, child) { - var node = child.parentNode; - while (node != null) { - if (node == parent) { - return true; - } - node = node.parentNode; - } - return false; -}; - -/** - * Generates a random hex string. Used as namespace for Handsontable instance events. - * @return {String} - 16 character random string: "92b1bfc74ec4" - */ -Handsontable.helper.randomString = function () { - return walkontableRandomString(); -}; - -/** - * Inherit without without calling parent constructor, and setting `Child.prototype.constructor` to `Child` instead of `Parent`. - * Creates temporary dummy function to call it as constructor. - * Described in ticket: https://github.com/handsontable/jquery-handsontable/pull/516 - * @param {Object} Child child class - * @param {Object} Parent parent class - * @return {Object} extended Child - */ -Handsontable.helper.inherit = function (Child, Parent) { - Parent.prototype.constructor = Parent; - Child.prototype = new Parent(); - Child.prototype.constructor = Child; - return Child; -}; - -/** - * Perform shallow extend of a target object with extension's own properties - * @param {Object} target An object that will receive the new properties - * @param {Object} extension An object containing additional properties to merge into the target - */ -Handsontable.helper.extend = function (target, extension) { - for (var i in extension) { - if (extension.hasOwnProperty(i)) { - target[i] = extension[i]; - } - } -}; - -Handsontable.helper.getPrototypeOf = function (obj) { - var prototype; - - if(typeof obj.__proto__ == "object"){ - prototype = obj.__proto__; - } else { - var oldConstructor, - constructor = obj.constructor; - - if (typeof obj.constructor == "function") { - oldConstructor = constructor; - - if (delete obj.constructor){ - constructor = obj.constructor; // get real constructor - obj.constructor = oldConstructor; // restore constructor - } - - - } - - prototype = constructor ? constructor.prototype : null; // needed for IE - - } - - return prototype; -}; - -/** - * Factory for columns constructors. - * @param {Object} GridSettings - * @param {Array} conflictList - * @return {Object} ColumnSettings - */ -Handsontable.helper.columnFactory = function (GridSettings, conflictList) { - function ColumnSettings () {} - - Handsontable.helper.inherit(ColumnSettings, GridSettings); - - // Clear conflict settings - for (var i = 0, len = conflictList.length; i < len; i++) { - ColumnSettings.prototype[conflictList[i]] = void 0; - } - - return ColumnSettings; -}; - -Handsontable.helper.translateRowsToColumns = function (input) { - var i - , ilen - , j - , jlen - , output = [] - , olen = 0; - - for (i = 0, ilen = input.length; i < ilen; i++) { - for (j = 0, jlen = input[i].length; j < jlen; j++) { - if (j == olen) { - output.push([]); - olen++; - } - output[j].push(input[i][j]) - } - } - return output; -}; - -Handsontable.helper.to2dArray = function (arr) { - var i = 0 - , ilen = arr.length; - while (i < ilen) { - arr[i] = [arr[i]]; - i++; - } -}; - -Handsontable.helper.extendArray = function (arr, extension) { - var i = 0 - , ilen = extension.length; - while (i < ilen) { - arr.push(extension[i]); - i++; - } -}; - -/** - * Determines if the given DOM element is an input field. - * Notice: By 'input' we mean input, textarea and select nodes - * @param element - DOM element - * @returns {boolean} - */ -Handsontable.helper.isInput = function (element) { - var inputs = ['INPUT', 'SELECT', 'TEXTAREA']; - - return inputs.indexOf(element.nodeName) > -1; -} - -/** - * Determines if the given DOM element is an input field placed OUTSIDE of HOT. - * Notice: By 'input' we mean input, textarea and select nodes - * @param element - DOM element - * @returns {boolean} - */ -Handsontable.helper.isOutsideInput = function (element) { - return Handsontable.helper.isInput(element) && element.className.indexOf('handsontableInput') == -1; -}; - -Handsontable.helper.keyCode = { - MOUSE_LEFT: 1, - MOUSE_RIGHT: 3, - MOUSE_MIDDLE: 2, - BACKSPACE: 8, - COMMA: 188, - DELETE: 46, - END: 35, - ENTER: 13, - ESCAPE: 27, - CONTROL_LEFT: 91, - COMMAND_LEFT: 17, - COMMAND_RIGHT: 93, - ALT: 18, - HOME: 36, - PAGE_DOWN: 34, - PAGE_UP: 33, - PERIOD: 190, - SPACE: 32, - SHIFT: 16, - CAPS_LOCK: 20, - TAB: 9, - ARROW_RIGHT: 39, - ARROW_LEFT: 37, - ARROW_UP: 38, - ARROW_DOWN: 40, - F1: 112, - F2: 113, - F3: 114, - F4: 115, - F5: 116, - F6: 117, - F7: 118, - F8: 119, - F9: 120, - F10: 121, - F11: 122, - F12: 123, - A: 65, - X: 88, - C: 67, - V: 86 -}; - -/** - * Determines whether given object is a plain Object. - * Note: String and Array are not plain Objects - * @param {*} obj - * @returns {boolean} - */ -Handsontable.helper.isObject = function (obj) { - return Object.prototype.toString.call(obj) == '[object Object]'; -}; - -/** - * Determines whether given object is an Array. - * Note: String is not an Array - * @param {*} obj - * @returns {boolean} - */ -Handsontable.helper.isArray = function(obj){ - return Array.isArray ? Array.isArray(obj) : Object.prototype.toString.call(obj) == '[object Array]'; -}; - -Handsontable.helper.pivot = function (arr) { - var pivotedArr = []; - - if(!arr || arr.length == 0 || !arr[0] || arr[0].length == 0){ - return pivotedArr; - } - - var rowCount = arr.length; - var colCount = arr[0].length; - - for(var i = 0; i < rowCount; i++){ - for(var j = 0; j < colCount; j++){ - if(!pivotedArr[j]){ - pivotedArr[j] = []; - } - - pivotedArr[j][i] = arr[i][j]; - } - } - - return pivotedArr; - -}; - -Handsontable.helper.proxy = function (fun, context) { - return function () { - return fun.apply(context, arguments); - }; -}; - -/** - * Factory that produces a function for searching methods (or any properties) which could be defined directly in - * table configuration or implicitly, within cell type definition. - * - * For example: renderer can be defined explicitly using "renderer" property in column configuration or it can be - * defined implicitly using "type" property. - * - * Methods/properties defined explicitly always takes precedence over those defined through "type". - * - * If the method/property is not found in an object, searching is continued recursively through prototype chain, until - * it reaches the Object.prototype. - * - * - * @param methodName {String} name of the method/property to search (i.e. 'renderer', 'validator', 'copyable') - * @param allowUndefined {Boolean} [optional] if false, the search is continued if methodName has not been found in cell "type" - * @returns {Function} - */ -Handsontable.helper.cellMethodLookupFactory = function (methodName, allowUndefined) { - - allowUndefined = typeof allowUndefined == 'undefined' ? true : allowUndefined; - - return function cellMethodLookup (row, col) { - - return (function getMethodFromProperties(properties) { - - if (!properties){ - - return; //method not found - - } - else if (properties.hasOwnProperty(methodName) && properties[methodName] !== void 0) { //check if it is own and is not empty - - return properties[methodName]; //method defined directly - - } else if (properties.hasOwnProperty('type') && properties.type) { //check if it is own and is not empty - - var type; - - if(typeof properties.type != 'string' ){ - throw new Error('Cell type must be a string '); - } - - type = translateTypeNameToObject(properties.type); - - if (type.hasOwnProperty(methodName)) { - return type[methodName]; //method defined in type. - } else if (allowUndefined) { - return; //method does not defined in type (eg. validator), returns undefined - } - - } - - return getMethodFromProperties(Handsontable.helper.getPrototypeOf(properties)); - - })(typeof row == 'number' ? this.getCellMeta(row, col) : row); - - }; - - function translateTypeNameToObject(typeName) { - var type = Handsontable.cellTypes[typeName]; - - if(typeof type == 'undefined'){ - throw new Error('You declared cell type "' + typeName + '" as a string that is not mapped to a known object. Cell type must be an object or a string mapped to an object in Handsontable.cellTypes'); - } - - return type; - } - -}; - -Handsontable.helper.toString = function (obj) { - return '' + obj; -}; - -(function (Handsontable) { - 'use strict'; - - /** - * Utility class that gets and saves data from/to the data source using mapping of columns numbers to object property names - * TODO refactor arguments of methods getRange, getText to be numbers (not objects) - * TODO remove priv, GridSettings from object constructor - * - * @param instance - * @param priv - * @param GridSettings - * @constructor - */ - Handsontable.DataMap = function (instance, priv, GridSettings) { - this.instance = instance; - this.priv = priv; - this.GridSettings = GridSettings; - this.dataSource = this.instance.getSettings().data; - - if (this.dataSource[0]) { - this.duckSchema = this.recursiveDuckSchema(this.dataSource[0]); - } - else { - this.duckSchema = {}; - } - this.createMap(); - }; - - Handsontable.DataMap.prototype.DESTINATION_RENDERER = 1; - Handsontable.DataMap.prototype.DESTINATION_CLIPBOARD_GENERATOR = 2; - - Handsontable.DataMap.prototype.recursiveDuckSchema = function (obj) { - var schema; - if ($.isPlainObject(obj)) { - schema = {}; - for (var i in obj) { - if (obj.hasOwnProperty(i)) { - if ($.isPlainObject(obj[i])) { - schema[i] = this.recursiveDuckSchema(obj[i]); - } - else { - schema[i] = null; - } - } - } - } - else { - schema = []; - } - return schema; - }; - - Handsontable.DataMap.prototype.recursiveDuckColumns = function (schema, lastCol, parent) { - var prop, i; - if (typeof lastCol === 'undefined') { - lastCol = 0; - parent = ''; - } - if ($.isPlainObject(schema)) { - for (i in schema) { - if (schema.hasOwnProperty(i)) { - if (schema[i] === null) { - prop = parent + i; - this.colToPropCache.push(prop); - this.propToColCache.set(prop, lastCol); - - lastCol++; - } - else { - lastCol = this.recursiveDuckColumns(schema[i], lastCol, i + '.'); - } - } - } - } - return lastCol; - }; - - Handsontable.DataMap.prototype.createMap = function () { - if (typeof this.getSchema() === "undefined") { - throw new Error("trying to create `columns` definition but you didnt' provide `schema` nor `data`"); - } - var i, ilen, schema = this.getSchema(); - this.colToPropCache = []; - this.propToColCache = new MultiMap(); - var columns = this.instance.getSettings().columns; - if (columns) { - for (i = 0, ilen = columns.length; i < ilen; i++) { - - if (typeof columns[i].data != 'undefined'){ - this.colToPropCache[i] = columns[i].data; - this.propToColCache.set(columns[i].data, i); - } - - } - } - else { - this.recursiveDuckColumns(schema); - } - }; - - Handsontable.DataMap.prototype.colToProp = function (col) { - col = Handsontable.hooks.execute(this.instance, 'modifyCol', col); - if (this.colToPropCache && typeof this.colToPropCache[col] !== 'undefined') { - return this.colToPropCache[col]; - } - else { - return col; - } - }; - - Handsontable.DataMap.prototype.propToCol = function (prop) { - var col; - if (typeof this.propToColCache.get(prop) !== 'undefined') { - col = this.propToColCache.get(prop); - } else { - col = prop; - } - col = Handsontable.hooks.execute(this.instance, 'modifyCol', col); - return col; - }; - - Handsontable.DataMap.prototype.getSchema = function () { - var schema = this.instance.getSettings().dataSchema; - if (schema) { - if (typeof schema === 'function') { - return schema(); - } - return schema; - } - return this.duckSchema; - }; - - /** - * Creates row at the bottom of the data array - * @param {Number} [index] Optional. Index of the row before which the new row will be inserted - */ - Handsontable.DataMap.prototype.createRow = function (index, amount, createdAutomatically) { - var row - , colCount = this.instance.countCols() - , numberOfCreatedRows = 0 - , currentIndex; - - if (!amount) { - amount = 1; - } - - if (typeof index !== 'number' || index >= this.instance.countRows()) { - index = this.instance.countRows(); - } - - currentIndex = index; - var maxRows = this.instance.getSettings().maxRows; - while (numberOfCreatedRows < amount && this.instance.countRows() < maxRows) { - - if (this.instance.dataType === 'array') { - row = []; - for (var c = 0; c < colCount; c++) { - row.push(null); - } - } - else if (this.instance.dataType === 'function') { - row = this.instance.getSettings().dataSchema(index); - } - else { - row = $.extend(true, {}, this.getSchema()); - } - - if (index === this.instance.countRows()) { - this.dataSource.push(row); - } - else { - this.dataSource.splice(index, 0, row); - } - - numberOfCreatedRows++; - currentIndex++; - } - - - Handsontable.hooks.run(this.instance, 'afterCreateRow', index, numberOfCreatedRows, createdAutomatically); - this.instance.forceFullRender = true; //used when data was changed - - return numberOfCreatedRows; - }; - - /** - * Creates col at the right of the data array - * @param {Number} [index] Optional. Index of the column before which the new column will be inserted - * * @param {Number} [amount] Optional. - */ - Handsontable.DataMap.prototype.createCol = function (index, amount, createdAutomatically) { - if (this.instance.dataType === 'object' || this.instance.getSettings().columns) { - throw new Error("Cannot create new column. When data source in an object, " + - "you can only have as much columns as defined in first data row, data schema or in the 'columns' setting." + - "If you want to be able to add new columns, you have to use array datasource."); - } - var rlen = this.instance.countRows() - , data = this.dataSource - , constructor - , numberOfCreatedCols = 0 - , currentIndex; - - if (!amount) { - amount = 1; - } - - currentIndex = index; - - var maxCols = this.instance.getSettings().maxCols; - while (numberOfCreatedCols < amount && this.instance.countCols() < maxCols) { - constructor = Handsontable.helper.columnFactory(this.GridSettings, this.priv.columnsSettingConflicts); - if (typeof index !== 'number' || index >= this.instance.countCols()) { - for (var r = 0; r < rlen; r++) { - if (typeof data[r] === 'undefined') { - data[r] = []; - } - data[r].push(null); - } - // Add new column constructor - this.priv.columnSettings.push(constructor); - } - else { - for (var r = 0; r < rlen; r++) { - data[r].splice(currentIndex, 0, null); - } - // Add new column constructor at given index - this.priv.columnSettings.splice(currentIndex, 0, constructor); - } - - numberOfCreatedCols++; - currentIndex++; - } - - Handsontable.hooks.run(this.instance, 'afterCreateCol', index, numberOfCreatedCols, createdAutomatically); - this.instance.forceFullRender = true; //used when data was changed - - return numberOfCreatedCols; - }; - - /** - * Removes row from the data array - * @param {Number} [index] Optional. Index of the row to be removed. If not provided, the last row will be removed - * @param {Number} [amount] Optional. Amount of the rows to be removed. If not provided, one row will be removed - */ - Handsontable.DataMap.prototype.removeRow = function (index, amount) { - if (!amount) { - amount = 1; - } - if (typeof index !== 'number') { - index = -amount; - } - - index = (this.instance.countRows() + index) % this.instance.countRows(); - - // We have to map the physical row ids to logical and than perform removing with (possibly) new row id - var logicRows = this.physicalRowsToLogical(index, amount); - - var actionWasNotCancelled = Handsontable.hooks.execute(this.instance, 'beforeRemoveRow', index, amount); - - if (actionWasNotCancelled === false) { - return; - } - - var data = this.dataSource; - var newData = data.filter(function (row, index) { - return logicRows.indexOf(index) == -1; - }); - - data.length = 0; - Array.prototype.push.apply(data, newData); - - Handsontable.hooks.run(this.instance, 'afterRemoveRow', index, amount); - - this.instance.forceFullRender = true; //used when data was changed - }; - - /** - * Removes column from the data array - * @param {Number} [index] Optional. Index of the column to be removed. If not provided, the last column will be removed - * @param {Number} [amount] Optional. Amount of the columns to be removed. If not provided, one column will be removed - */ - Handsontable.DataMap.prototype.removeCol = function (index, amount) { - if (this.instance.dataType === 'object' || this.instance.getSettings().columns) { - throw new Error("cannot remove column with object data source or columns option specified"); - } - if (!amount) { - amount = 1; - } - if (typeof index !== 'number') { - index = -amount; - } - - index = (this.instance.countCols() + index) % this.instance.countCols(); - - var actionWasNotCancelled = Handsontable.hooks.execute(this.instance, 'beforeRemoveCol', index, amount); - - if (actionWasNotCancelled === false) { - return; - } - - var data = this.dataSource; - for (var r = 0, rlen = this.instance.countRows(); r < rlen; r++) { - data[r].splice(index, amount); - } - this.priv.columnSettings.splice(index, amount); - - Handsontable.hooks.run(this.instance, 'afterRemoveCol', index, amount); - this.instance.forceFullRender = true; //used when data was changed - }; - - /** - * Add / removes data from the column - * @param {Number} col Index of column in which do you want to do splice. - * @param {Number} index Index at which to start changing the array. If negative, will begin that many elements from the end - * @param {Number} amount An integer indicating the number of old array elements to remove. If amount is 0, no elements are removed - * param {...*} elements Optional. The elements to add to the array. If you don't specify any elements, spliceCol simply removes elements from the array - */ - Handsontable.DataMap.prototype.spliceCol = function (col, index, amount/*, elements...*/) { - var elements = 4 <= arguments.length ? [].slice.call(arguments, 3) : []; - - var colData = this.instance.getDataAtCol(col); - var removed = colData.slice(index, index + amount); - var after = colData.slice(index + amount); - - Handsontable.helper.extendArray(elements, after); - var i = 0; - while (i < amount) { - elements.push(null); //add null in place of removed elements - i++; - } - Handsontable.helper.to2dArray(elements); - this.instance.populateFromArray(index, col, elements, null, null, 'spliceCol'); - - return removed; - }; - - /** - * Add / removes data from the row - * @param {Number} row Index of row in which do you want to do splice. - * @param {Number} index Index at which to start changing the array. If negative, will begin that many elements from the end - * @param {Number} amount An integer indicating the number of old array elements to remove. If amount is 0, no elements are removed - * param {...*} elements Optional. The elements to add to the array. If you don't specify any elements, spliceCol simply removes elements from the array - */ - Handsontable.DataMap.prototype.spliceRow = function (row, index, amount/*, elements...*/) { - var elements = 4 <= arguments.length ? [].slice.call(arguments, 3) : []; - - var rowData = this.instance.getSourceDataAtRow(row); - var removed = rowData.slice(index, index + amount); - var after = rowData.slice(index + amount); - - Handsontable.helper.extendArray(elements, after); - var i = 0; - while (i < amount) { - elements.push(null); //add null in place of removed elements - i++; - } - this.instance.populateFromArray(row, index, [elements], null, null, 'spliceRow'); - - return removed; - }; - - /** - * Returns single value from the data array - * @param {Number} row - * @param {Number} prop - */ - Handsontable.DataMap.prototype.get = function (row, prop) { - row = Handsontable.hooks.execute(this.instance, 'modifyRow', row); - if (typeof prop === 'string' && prop.indexOf('.') > -1) { - var sliced = prop.split("."); - var out = this.dataSource[row]; - if (!out) { - return null; - } - for (var i = 0, ilen = sliced.length; i < ilen; i++) { - out = out[sliced[i]]; - if (typeof out === 'undefined') { - return null; - } - } - return out; - } - else if (typeof prop === 'function') { - /** - * allows for interacting with complex structures, for example - * d3/jQuery getter/setter properties: - * - * {columns: [{ - * data: function(row, value){ - * if(arguments.length === 1){ - * return row.property(); - * } - * row.property(value); - * } - * }]} - */ - return prop(this.dataSource.slice( - row, - row + 1 - )[0]); - } - else { - return this.dataSource[row] ? this.dataSource[row][prop] : null; - } - }; - - var copyableLookup = Handsontable.helper.cellMethodLookupFactory('copyable', false); - - /** - * Returns single value from the data array (intended for clipboard copy to an external application) - * @param {Number} row - * @param {Number} prop - * @return {String} - */ - Handsontable.DataMap.prototype.getCopyable = function (row, prop) { - if (copyableLookup.call(this.instance, row, this.propToCol(prop))) { - return this.get(row, prop); - } - return ''; - }; - - /** - * Saves single value to the data array - * @param {Number} row - * @param {Number} prop - * @param {String} value - * @param {String} [source] Optional. Source of hook runner. - */ - Handsontable.DataMap.prototype.set = function (row, prop, value, source) { - row = Handsontable.hooks.execute(this.instance, 'modifyRow', row, source || "datamapGet"); - if (typeof prop === 'string' && prop.indexOf('.') > -1) { - var sliced = prop.split("."); - var out = this.dataSource[row]; - for (var i = 0, ilen = sliced.length - 1; i < ilen; i++) { - out = out[sliced[i]]; - } - out[sliced[i]] = value; - } - else if (typeof prop === 'function') { - /* see the `function` handler in `get` */ - prop(this.dataSource.slice( - row, - row + 1 - )[0], value); - } - else { - this.dataSource[row][prop] = value; - } - }; - - /** - * This ridiculous piece of code maps rows Id that are present in table data to those displayed for user. - * The trick is, the physical row id (stored in settings.data) is not necessary the same - * as the logical (displayed) row id (e.g. when sorting is applied). - */ - Handsontable.DataMap.prototype.physicalRowsToLogical = function (index, amount) { - var totalRows = this.instance.countRows(); - var physicRow = (totalRows + index) % totalRows; - var logicRows = []; - var rowsToRemove = amount; - var row; - - while (physicRow < totalRows && rowsToRemove) { - row = Handsontable.hooks.execute(this.instance, 'modifyRow', physicRow); - logicRows.push(row); - - rowsToRemove--; - physicRow++; - } - - return logicRows; - }; - - /** - * Clears the data array - */ - Handsontable.DataMap.prototype.clear = function () { - for (var r = 0; r < this.instance.countRows(); r++) { - for (var c = 0; c < this.instance.countCols(); c++) { - this.set(r, this.colToProp(c), ''); - } - } - }; - - /** - * Returns the data array - * @return {Array} - */ - Handsontable.DataMap.prototype.getAll = function () { - return this.dataSource; - }; - - /** - * Returns data range as array - * @param {Object} start Start selection position - * @param {Object} end End selection position - * @param {Number} destination Destination of datamap.get - * @return {Array} - */ - Handsontable.DataMap.prototype.getRange = function (start, end, destination) { - var r, rlen, c, clen, output = [], row; - var getFn = destination === this.DESTINATION_CLIPBOARD_GENERATOR ? this.getCopyable : this.get; - rlen = Math.max(start.row, end.row); - clen = Math.max(start.col, end.col); - for (r = Math.min(start.row, end.row); r <= rlen; r++) { - row = []; - for (c = Math.min(start.col, end.col); c <= clen; c++) { - row.push(getFn.call(this, r, this.colToProp(c))); - } - output.push(row); - } - return output; - }; - - /** - * Return data as text (tab separated columns) - * @param {Object} start (Optional) Start selection position - * @param {Object} end (Optional) End selection position - * @return {String} - */ - Handsontable.DataMap.prototype.getText = function (start, end) { - return SheetClip.stringify(this.getRange(start, end, this.DESTINATION_RENDERER)); - }; - - /** - * Return data as copyable text (tab separated columns intended for clipboard copy to an external application) - * @param {Object} start (Optional) Start selection position - * @param {Object} end (Optional) End selection position - * @return {String} - */ - Handsontable.DataMap.prototype.getCopyableText = function (start, end) { - return SheetClip.stringify(this.getRange(start, end, this.DESTINATION_CLIPBOARD_GENERATOR)); - }; - -})(Handsontable); - -(function (Handsontable) { - 'use strict'; - - /* - Adds appropriate CSS class to table cell, based on cellProperties - */ - Handsontable.renderers.cellDecorator = function (instance, TD, row, col, prop, value, cellProperties) { - if (cellProperties.className) { - if(TD.className) { - TD.className = TD.className + " " + cellProperties.className; - } else { - TD.className = cellProperties.className; - } - - } - - if (cellProperties.readOnly) { - Handsontable.Dom.addClass(TD, cellProperties.readOnlyCellClassName); - } - - if (cellProperties.valid === false && cellProperties.invalidCellClassName) { - Handsontable.Dom.addClass(TD, cellProperties.invalidCellClassName); - } - - if (cellProperties.wordWrap === false && cellProperties.noWordWrapClassName) { - Handsontable.Dom.addClass(TD, cellProperties.noWordWrapClassName); - } - - if (!value && cellProperties.placeholder) { - Handsontable.Dom.addClass(TD, cellProperties.placeholderCellClassName); - } - } - -})(Handsontable); -/** - * Default text renderer - * @param {Object} instance Handsontable instance - * @param {Element} TD Table cell where to render - * @param {Number} row - * @param {Number} col - * @param {String|Number} prop Row object property name - * @param value Value to render (remember to escape unsafe HTML before inserting to DOM!) - * @param {Object} cellProperties Cell properites (shared by cell renderer and editor) - */ -(function (Handsontable) { - 'use strict'; - - var TextRenderer = function (instance, TD, row, col, prop, value, cellProperties) { - - Handsontable.renderers.cellDecorator.apply(this, arguments); - - if (!value && cellProperties.placeholder) { - value = cellProperties.placeholder; - } - - var escaped = Handsontable.helper.stringify(value); - - if (cellProperties.rendererTemplate) { - Handsontable.Dom.empty(TD); - var TEMPLATE = document.createElement('TEMPLATE'); - TEMPLATE.setAttribute('bind', '{{}}'); - TEMPLATE.innerHTML = cellProperties.rendererTemplate; - HTMLTemplateElement.decorate(TEMPLATE); - TEMPLATE.model = instance.getSourceDataAtRow(row); - TD.appendChild(TEMPLATE); - } - else { - Handsontable.Dom.fastInnerText(TD, escaped); //this is faster than innerHTML. See: https://github.com/handsontable/jquery-handsontable/wiki/JavaScript-&-DOM-performance-tips - } - - }; - - //Handsontable.TextRenderer = TextRenderer; //Left for backward compatibility - Handsontable.renderers.TextRenderer = TextRenderer; - Handsontable.renderers.registerRenderer('text', TextRenderer); - -})(Handsontable); - -(function (Handsontable) { - - var clonableWRAPPER = document.createElement('DIV'); - clonableWRAPPER.className = 'htAutocompleteWrapper'; - - var clonableARROW = document.createElement('DIV'); - clonableARROW.className = 'htAutocompleteArrow'; - clonableARROW.appendChild(document.createTextNode('\u25BC')); -//this is faster than innerHTML. See: https://github.com/handsontable/jquery-handsontable/wiki/JavaScript-&-DOM-performance-tips - - var wrapTdContentWithWrapper = function(TD, WRAPPER){ - WRAPPER.innerHTML = TD.innerHTML; - Handsontable.Dom.empty(TD); - TD.appendChild(WRAPPER); - }; - - /** - * Autocomplete renderer - * @param {Object} instance Handsontable instance - * @param {Element} TD Table cell where to render - * @param {Number} row - * @param {Number} col - * @param {String|Number} prop Row object property name - * @param value Value to render (remember to escape unsafe HTML before inserting to DOM!) - * @param {Object} cellProperties Cell properites (shared by cell renderer and editor) - */ - var AutocompleteRenderer = function (instance, TD, row, col, prop, value, cellProperties) { - - var WRAPPER = clonableWRAPPER.cloneNode(true); //this is faster than createElement - var ARROW = clonableARROW.cloneNode(true); //this is faster than createElement - - Handsontable.renderers.TextRenderer(instance, TD, row, col, prop, value, cellProperties); - - TD.appendChild(ARROW); - Handsontable.Dom.addClass(TD, 'htAutocomplete'); - - - if (!TD.firstChild) { //http://jsperf.com/empty-node-if-needed - //otherwise empty fields appear borderless in demo/renderers.html (IE) - TD.appendChild(document.createTextNode('\u00A0')); //\u00A0 equals   for a text node - //this is faster than innerHTML. See: https://github.com/handsontable/jquery-handsontable/wiki/JavaScript-&-DOM-performance-tips - } - - if (!instance.acArrowListener) { - //not very elegant but easy and fast - instance.acArrowListener = function () { - instance.view.wt.getSetting('onCellDblClick', null, new WalkontableCellCoords(row, col), TD); - }; - - instance.rootElement.on('mousedown.htAutocompleteArrow', '.htAutocompleteArrow', instance.acArrowListener); //this way we don't bind event listener to each arrow. We rely on propagation instead - - //We need to unbind the listener after the table has been destroyed - instance.addHookOnce('afterDestroy', function () { - this.rootElement.off('mousedown.htAutocompleteArrow'); - }); - - } - }; - - Handsontable.AutocompleteRenderer = AutocompleteRenderer; - Handsontable.renderers.AutocompleteRenderer = AutocompleteRenderer; - Handsontable.renderers.registerRenderer('autocomplete', AutocompleteRenderer); -})(Handsontable); -/** - * Checkbox renderer - * @param {Object} instance Handsontable instance - * @param {Element} TD Table cell where to render - * @param {Number} row - * @param {Number} col - * @param {String|Number} prop Row object property name - * @param value Value to render (remember to escape unsafe HTML before inserting to DOM!) - * @param {Object} cellProperties Cell properites (shared by cell renderer and editor) - */ -(function (Handsontable) { - - 'use strict'; - - var clonableINPUT = document.createElement('INPUT'); - clonableINPUT.className = 'htCheckboxRendererInput'; - clonableINPUT.type = 'checkbox'; - clonableINPUT.setAttribute('autocomplete', 'off'); - - var CheckboxRenderer = function (instance, TD, row, col, prop, value, cellProperties) { - - if (typeof cellProperties.checkedTemplate === "undefined") { - cellProperties.checkedTemplate = true; - } - if (typeof cellProperties.uncheckedTemplate === "undefined") { - cellProperties.uncheckedTemplate = false; - } - - Handsontable.Dom.empty(TD); //TODO identify under what circumstances this line can be removed - - var INPUT = clonableINPUT.cloneNode(false); //this is faster than createElement - - if (value === cellProperties.checkedTemplate || value === Handsontable.helper.stringify(cellProperties.checkedTemplate)) { - INPUT.checked = true; - TD.appendChild(INPUT); - } - else if (value === cellProperties.uncheckedTemplate || value === Handsontable.helper.stringify(cellProperties.uncheckedTemplate)) { - TD.appendChild(INPUT); - } - else if (value === null) { //default value - INPUT.className += ' noValue'; - TD.appendChild(INPUT); - } - else { - Handsontable.Dom.fastInnerText(TD, '#bad value#'); //this is faster than innerHTML. See: https://github.com/handsontable/jquery-handsontable/wiki/JavaScript-&-DOM-performance-tips - } - - var $input = $(INPUT); - - if (cellProperties.readOnly) { - $input.on('click', function (event) { - event.preventDefault(); - }); - } - else { - $input.on('mousedown', function (event) { - event.stopPropagation(); //otherwise can confuse cell mousedown handler - }); - - $input.on('mouseup', function (event) { - event.stopPropagation(); //otherwise can confuse cell dblclick handler - }); - - $input.on('change', function(){ - if (this.checked) { - instance.setDataAtRowProp(row, prop, cellProperties.checkedTemplate); - } - else { - instance.setDataAtRowProp(row, prop, cellProperties.uncheckedTemplate); - } - }); - } - - if(!instance.CheckboxRenderer || !instance.CheckboxRenderer.beforeKeyDownHookBound){ - instance.CheckboxRenderer = { - beforeKeyDownHookBound : true - }; - - instance.addHook('beforeKeyDown', function(event){ - if(event.keyCode == Handsontable.helper.keyCode.SPACE){ - - var cell, checkbox, cellProperties; - - var selRange = instance.getSelectedRange(); - var topLeft = selRange.getTopLeftCorner(); - var bottomRight = selRange.getBottomRightCorner(); - - for(var row = topLeft.row; row <= bottomRight.row; row++ ){ - for(var col = topLeft.col; col <= bottomRight.col; col++){ - cell = instance.getCell(row, col); - cellProperties = instance.getCellMeta(row, col); - - checkbox = cell.querySelectorAll('input[type=checkbox]'); - - if(checkbox.length > 0 && !cellProperties.readOnly){ - - if(!event.isImmediatePropagationStopped()){ - event.stopImmediatePropagation(); - event.preventDefault(); - } - - for(var i = 0, len = checkbox.length; i < len; i++){ - checkbox[i].checked = !checkbox[i].checked; - $(checkbox[i]).trigger('change'); - } - - } - - } - } - } - }); - } - - }; - - Handsontable.CheckboxRenderer = CheckboxRenderer; - Handsontable.renderers.CheckboxRenderer = CheckboxRenderer; - Handsontable.renderers.registerRenderer('checkbox', CheckboxRenderer); - -})(Handsontable); -/** - * Numeric cell renderer - * @param {Object} instance Handsontable instance - * @param {Element} TD Table cell where to render - * @param {Number} row - * @param {Number} col - * @param {String|Number} prop Row object property name - * @param value Value to render (remember to escape unsafe HTML before inserting to DOM!) - * @param {Object} cellProperties Cell properites (shared by cell renderer and editor) - */ -(function (Handsontable) { - - 'use strict'; - - var NumericRenderer = function (instance, TD, row, col, prop, value, cellProperties) { - if (Handsontable.helper.isNumeric(value)) { - if (typeof cellProperties.language !== 'undefined') { - numeral.language(cellProperties.language) - } - value = numeral(value).format(cellProperties.format || '0'); //docs: http://numeraljs.com/ - Handsontable.Dom.addClass(TD, 'htNumeric'); - } - Handsontable.renderers.TextRenderer(instance, TD, row, col, prop, value, cellProperties); - }; - - Handsontable.NumericRenderer = NumericRenderer; //Left for backward compatibility with versions prior 0.10.0 - Handsontable.renderers.NumericRenderer = NumericRenderer; - Handsontable.renderers.registerRenderer('numeric', NumericRenderer); - -})(Handsontable); -(function(Handosntable){ - - 'use strict'; - - var PasswordRenderer = function (instance, TD, row, col, prop, value, cellProperties) { - Handsontable.renderers.TextRenderer.apply(this, arguments); - - value = TD.innerHTML; - - var hash; - var hashLength = cellProperties.hashLength || value.length; - var hashSymbol = cellProperties.hashSymbol || '*'; - - for( hash = ''; hash.split(hashSymbol).length - 1 < hashLength; hash += hashSymbol); - - Handsontable.Dom.fastInnerHTML(TD, hash); - - }; - - Handosntable.PasswordRenderer = PasswordRenderer; - Handosntable.renderers.PasswordRenderer = PasswordRenderer; - Handosntable.renderers.registerRenderer('password', PasswordRenderer); - -})(Handsontable); -(function (Handsontable) { - - function HtmlRenderer(instance, TD, row, col, prop, value, cellProperties){ - - Handsontable.renderers.cellDecorator.apply(this, arguments); - - Handsontable.Dom.fastInnerHTML(TD, value); - } - - Handsontable.renderers.registerRenderer('html', HtmlRenderer); - Handsontable.renderers.HtmlRenderer = HtmlRenderer; - -})(Handsontable); - -(function (Handsontable) { - 'use strict'; - - Handsontable.EditorState = { - VIRGIN: 'STATE_VIRGIN', //before editing - EDITING: 'STATE_EDITING', - WAITING: 'STATE_WAITING', //waiting for async validation - FINISHED: 'STATE_FINISHED' - }; - - function BaseEditor(instance) { - this.instance = instance; - this.state = Handsontable.EditorState.VIRGIN; - - this._opened = false; - this._closeCallback = null; - - this.init(); - } - - BaseEditor.prototype._fireCallbacks = function(result) { - if(this._closeCallback){ - this._closeCallback(result); - this._closeCallback = null; - } - - } - - BaseEditor.prototype.init = function(){}; - - BaseEditor.prototype.getValue = function(){ - throw Error('Editor getValue() method unimplemented'); - }; - - BaseEditor.prototype.setValue = function(newValue){ - throw Error('Editor setValue() method unimplemented'); - }; - - BaseEditor.prototype.open = function(){ - throw Error('Editor open() method unimplemented'); - }; - - BaseEditor.prototype.close = function(){ - throw Error('Editor close() method unimplemented'); - }; - - BaseEditor.prototype.prepare = function(row, col, prop, td, originalValue, cellProperties){ - this.TD = td; - this.row = row; - this.col = col; - this.prop = prop; - this.originalValue = originalValue; - this.cellProperties = cellProperties; - - this.state = Handsontable.EditorState.VIRGIN; - }; - - BaseEditor.prototype.extend = function(){ - var baseClass = this.constructor; - function Editor(){ - baseClass.apply(this, arguments); - } - - function inherit(Child, Parent){ - function Bridge() { - } - - Bridge.prototype = Parent.prototype; - Child.prototype = new Bridge(); - Child.prototype.constructor = Child; - return Child; - } - - return inherit(Editor, baseClass); - }; - - BaseEditor.prototype.saveValue = function (val, ctrlDown) { - if (ctrlDown) { //if ctrl+enter and multiple cells selected, behave like Excel (finish editing and apply to all cells) - var sel = this.instance.getSelected(); - this.instance.populateFromArray(sel[0], sel[1], val, sel[2], sel[3], 'edit'); - } - else { - this.instance.populateFromArray(this.row, this.col, val, null, null, 'edit'); - } - }; - - BaseEditor.prototype.beginEditing = function(initialValue){ - if (this.state != Handsontable.EditorState.VIRGIN) { - return; - } - - this.instance.view.scrollViewport(new WalkontableCellCoords(this.row, this.col)); - this.instance.view.render(); - - this.state = Handsontable.EditorState.EDITING; - - initialValue = typeof initialValue == 'string' ? initialValue : this.originalValue; - - this.setValue(Handsontable.helper.stringify(initialValue)); - - this.open(); - this._opened = true; - this.focus(); - - this.instance.view.render(); //only rerender the selections (FillHandle should disappear when beginediting is triggered) - }; - - BaseEditor.prototype.finishEditing = function (restoreOriginalValue, ctrlDown, callback) { - - if (callback) { - var previousCloseCallback = this._closeCallback; - this._closeCallback = function (result) { - if(previousCloseCallback){ - previousCloseCallback(result); - } - - callback(result); - }; - } - - if (this.isWaiting()) { - return; - } - - if (this.state == Handsontable.EditorState.VIRGIN) { - var that = this; - this.instance._registerTimeout(setTimeout(function () { - that._fireCallbacks(true); - }, 0)); - return; - } - - if (this.state == Handsontable.EditorState.EDITING) { - - if (restoreOriginalValue) { - - this.cancelChanges(); - return; - - } - - - var val = [ - [String.prototype.trim.call(this.getValue())] //String.prototype.trim is defined in Walkontable polyfill.js - ]; - - this.state = Handsontable.EditorState.WAITING; - - this.saveValue(val, ctrlDown); - - if(this.instance.getCellValidator(this.cellProperties)){ - var that = this; - this.instance.addHookOnce('afterValidate', function (result) { - that.state = Handsontable.EditorState.FINISHED; - that.discardEditor(result); - }); - } else { - this.state = Handsontable.EditorState.FINISHED; - this.discardEditor(true); - } - - } - }; - - BaseEditor.prototype.cancelChanges = function () { - this.state = Handsontable.EditorState.FINISHED; - this.discardEditor(); - }; - - BaseEditor.prototype.discardEditor = function (result) { - if (this.state !== Handsontable.EditorState.FINISHED) { - return; - } - - if (result === false && this.cellProperties.allowInvalid !== true) { //validator was defined and failed - - this.instance.selectCell(this.row, this.col); - this.focus(); - - this.state = Handsontable.EditorState.EDITING; - - this._fireCallbacks(false); - } - else { - this.close(); - this._opened = false; - - this.state = Handsontable.EditorState.VIRGIN; - - this._fireCallbacks(true); - } - - }; - - BaseEditor.prototype.isOpened = function(){ - return this._opened; - }; - - BaseEditor.prototype.isWaiting = function () { - return this.state === Handsontable.EditorState.WAITING; - }; - - Handsontable.editors.BaseEditor = BaseEditor; - -})(Handsontable); - -(function(Handsontable){ - var TextEditor = Handsontable.editors.BaseEditor.prototype.extend(); - - TextEditor.prototype.init = function(){ - this.createElements(); - this.bindEvents(); - this.autoResize = autoResize(); - }; - - TextEditor.prototype.getValue = function(){ - return this.TEXTAREA.value - }; - - TextEditor.prototype.setValue = function(newValue){ - this.TEXTAREA.value = newValue; - }; - - var onBeforeKeyDown = function onBeforeKeyDown(event){ - - var instance = this; - var that = instance.getActiveEditor(); - - var keyCodes = Handsontable.helper.keyCode; - var ctrlDown = (event.ctrlKey || event.metaKey) && !event.altKey; //catch CTRL but not right ALT (which in some systems triggers ALT+CTRL) - - - //Process only events that have been fired in the editor - if (event.target !== that.TEXTAREA || event.isImmediatePropagationStopped()){ - return; - } - - if (event.keyCode === 17 || event.keyCode === 224 || event.keyCode === 91 || event.keyCode === 93) { - //when CTRL or its equivalent is pressed and cell is edited, don't prepare selectable text in textarea - event.stopImmediatePropagation(); - return; - } - - switch (event.keyCode) { - case keyCodes.ARROW_RIGHT: - if (Handsontable.Dom.getCaretPosition(that.TEXTAREA) !== that.TEXTAREA.value.length) { - event.stopImmediatePropagation(); - } - break; - - case keyCodes.ARROW_LEFT: /* arrow left */ - if (Handsontable.Dom.getCaretPosition(that.TEXTAREA) !== 0) { - event.stopImmediatePropagation(); - } - break; - - case keyCodes.ENTER: - var selected = that.instance.getSelected(); - var isMultipleSelection = !(selected[0] === selected[2] && selected[1] === selected[3]); - if ((ctrlDown && !isMultipleSelection) || event.altKey) { //if ctrl+enter or alt+enter, add new line - if(that.isOpened()){ - that.setValue(that.getValue() + '\n'); - that.focus(); - } else { - that.beginEditing(that.originalValue + '\n') - } - event.stopImmediatePropagation(); - } - event.preventDefault(); //don't add newline to field - break; - - case keyCodes.A: - case keyCodes.X: - case keyCodes.C: - case keyCodes.V: - if(ctrlDown){ - event.stopImmediatePropagation(); //CTRL+A, CTRL+C, CTRL+V, CTRL+X should only work locally when cell is edited (not in table context) - break; - } - case keyCodes.BACKSPACE: - case keyCodes.DELETE: - case keyCodes.HOME: - case keyCodes.END: - event.stopImmediatePropagation(); //backspace, delete, home, end should only work locally when cell is edited (not in table context) - break; - } - - }; - - TextEditor.prototype.open = function(){ - this.refreshDimensions(); //need it instantly, to prevent https://github.com/handsontable/jquery-handsontable/issues/348 - - this.instance.addHook('beforeKeyDown', onBeforeKeyDown); - }; - - TextEditor.prototype.close = function(){ - this.textareaParentStyle.display = 'none'; - - this.autoResize.unObserve(); - - if (document.activeElement === this.TEXTAREA) { - this.instance.listen(); //don't refocus the table if user focused some cell outside of HT on purpose - } - - this.instance.removeHook('beforeKeyDown', onBeforeKeyDown); - }; - - TextEditor.prototype.focus = function(){ - this.TEXTAREA.focus(); - Handsontable.Dom.setCaretPosition(this.TEXTAREA, this.TEXTAREA.value.length); - }; - - TextEditor.prototype.createElements = function () { - this.$body = $(document.body); - - this.TEXTAREA = document.createElement('TEXTAREA'); - this.$textarea = $(this.TEXTAREA); - - Handsontable.Dom.addClass(this.TEXTAREA, 'handsontableInput'); - - this.textareaStyle = this.TEXTAREA.style; - this.textareaStyle.width = 0; - this.textareaStyle.height = 0; - - this.TEXTAREA_PARENT = document.createElement('DIV'); - Handsontable.Dom.addClass(this.TEXTAREA_PARENT, 'handsontableInputHolder'); - - this.textareaParentStyle = this.TEXTAREA_PARENT.style; - this.textareaParentStyle.top = 0; - this.textareaParentStyle.left = 0; - this.textareaParentStyle.display = 'none'; - - this.TEXTAREA_PARENT.appendChild(this.TEXTAREA); - - this.instance.rootElement[0].appendChild(this.TEXTAREA_PARENT); - - var that = this; - this.instance._registerTimeout(setTimeout(function () { - that.refreshDimensions(); - }, 0)); - }; - - TextEditor.prototype.checkEditorSection = function () { - if(this.row < this.instance.getSettings().fixedRowsTop) { - if(this.col < this.instance.getSettings().fixedColumnsLeft) { - return 'corner'; - } else { - return 'top'; - } - } else { - if(this.col < this.instance.getSettings().fixedColumnsLeft) { - return 'left'; - } - } - }; - - TextEditor.prototype.getEditedCell = function () { - var editorSection = this.checkEditorSection() - , editedCell; - - switch (editorSection) { - case 'top': - editedCell = this.instance.view.wt.wtScrollbars.vertical.clone.wtTable.getCell({row: this.row, col: this.col}); - this.textareaParentStyle.zIndex = 101; - break; - case 'corner': - editedCell = this.instance.view.wt.wtScrollbars.corner.clone.wtTable.getCell({row: this.row, col: this.col}); - this.textareaParentStyle.zIndex = 103; - break; - case 'left': - editedCell = this.instance.view.wt.wtScrollbars.horizontal.clone.wtTable.getCell({row: this.row, col: this.col}); - this.textareaParentStyle.zIndex = 102; - break; - default : - editedCell = this.instance.getCell(this.row, this.col); - this.textareaParentStyle.zIndex = ""; - break; - } - - return editedCell != -1 && editedCell != -2 ? editedCell : void 0; - }; - - - TextEditor.prototype.refreshDimensions = function () { - if (this.state !== Handsontable.EditorState.EDITING) { - return; - } - - ///start prepare textarea position -// this.TD = this.instance.getCell(this.row, this.col); - this.TD = this.getEditedCell(); - - if (!this.TD) { - //TD is outside of the viewport. Otherwise throws exception when scrolling the table while a cell is edited - return; - } - var $td = $(this.TD); //because old td may have been scrolled out with scrollViewport - var currentOffset = Handsontable.Dom.offset(this.TD); - var containerOffset = Handsontable.Dom.offset(this.instance.rootElement[0]); - var editTop = currentOffset.top - containerOffset.top - 1; - var editLeft = currentOffset.left - containerOffset.left - 1; - - var settings = this.instance.getSettings(); - var rowHeadersCount = settings.rowHeaders === false ? 0 : 1; - var colHeadersCount = settings.colHeaders === false ? 0 : 1; - var editorSection = this.checkEditorSection(); - - - if (editTop < 0) { - editTop = 0; - } - if (editLeft < 0) { - editLeft = 0; - } - - if (rowHeadersCount > 0 && parseInt($td.css('border-top-width'), 10) > 0) { - editTop += 1; - } - if (colHeadersCount > 0 && parseInt($td.css('border-left-width'), 10) > 0) { - editLeft += 1; - } - - this.textareaParentStyle.top = editTop + 'px'; - this.textareaParentStyle.left = editLeft + 'px'; - - ///end prepare textarea position - - - var cellTopOffset = this.TD.offsetTop, - cellLeftOffset = this.TD.offsetLeft - this.instance.view.wt.wtScrollbars.horizontal.getScrollPosition(); - - var width = $td.width() - , maxWidth = this.instance.view.maximumVisibleElementWidth(cellLeftOffset) - 10 //10 is TEXTAREAs border and padding - , height = $td.outerHeight() - 4 - , maxHeight = this.instance.view.maximumVisibleElementHeight(cellTopOffset)-2; //10 is TEXTAREAs border and padding - - if (parseInt($td.css('border-top-width'), 10) > 0) { - height -= 1; - } - if (parseInt($td.css('border-left-width'), 10) > 0) { - if (rowHeadersCount > 0) { - width -= 1; - } - } - - this.autoResize.init(this.$textarea[0], { - fontSize: parseInt( window.getComputedStyle($td[0]).fontSize,10), - minHeight: Math.min(height, maxHeight), - maxHeight: maxHeight, //TEXTAREA should never be wider than visible part of the viewport (should not cover the scrollbar) - minWidth: Math.min(width, maxWidth), - maxWidth: maxWidth //TEXTAREA should never be wider than visible part of the viewport (should not cover the scrollbar) - }); - - this.textareaParentStyle.display = 'block'; - }; - - TextEditor.prototype.bindEvents = function () { - var editor = this; - - this.$textarea.on('cut.editor', function (event) { - event.stopPropagation(); - }); - - this.$textarea.on('paste.editor', function (event) { - event.stopPropagation(); - }); - - this.instance.addHook('afterScrollVertically', function () { - editor.refreshDimensions(); - }); - }; - - - Handsontable.editors.TextEditor = TextEditor; - Handsontable.editors.registerEditor('text', Handsontable.editors.TextEditor); - -})(Handsontable); - -(function(Handsontable){ - - //Blank editor, because all the work is done by renderer - var CheckboxEditor = Handsontable.editors.BaseEditor.prototype.extend(); - - CheckboxEditor.prototype.beginEditing = function () { - var checkbox = this.TD.querySelector('input[type="checkbox"]'); - - if (checkbox) { - $(checkbox).trigger('click'); - } - - }; - - CheckboxEditor.prototype.finishEditing = function () {}; - - CheckboxEditor.prototype.init = function () {}; - CheckboxEditor.prototype.open = function () {}; - CheckboxEditor.prototype.close = function () {}; - CheckboxEditor.prototype.getValue = function () {}; - CheckboxEditor.prototype.setValue = function () {}; - CheckboxEditor.prototype.focus = function () {}; - - Handsontable.editors.CheckboxEditor = CheckboxEditor; - Handsontable.editors.registerEditor('checkbox', CheckboxEditor); - -})(Handsontable); - - -(function (Handsontable) { - var DateEditor = Handsontable.editors.TextEditor.prototype.extend(); - - DateEditor.prototype.init = function () { - if (!$.datepicker) { - throw new Error("jQuery UI Datepicker dependency not found. Did you forget to include jquery-ui.custom.js or its substitute?"); - } - - Handsontable.editors.TextEditor.prototype.init.apply(this, arguments); - - this.isCellEdited = false; - var that = this; - - this.instance.addHook('afterDestroy', function () { - that.destroyElements(); - }) - - }; - - DateEditor.prototype.createElements = function () { - Handsontable.editors.TextEditor.prototype.createElements.apply(this, arguments); - - this.datePicker = document.createElement('DIV'); - Handsontable.Dom.addClass(this.datePicker, 'htDatepickerHolder'); - this.datePickerStyle = this.datePicker.style; - this.datePickerStyle.position = 'absolute'; - this.datePickerStyle.top = 0; - this.datePickerStyle.left = 0; - this.datePickerStyle.zIndex = 99; - document.body.appendChild(this.datePicker); - this.$datePicker = $(this.datePicker); - - var that = this; - var defaultOptions = { - dateFormat: "yy-mm-dd", - showButtonPanel: true, - changeMonth: true, - changeYear: true, - onSelect: function (dateStr) { - that.setValue(dateStr); - that.finishEditing(false); - } - }; - this.$datePicker.datepicker(defaultOptions); - - /** - * Prevent recognizing clicking on jQuery Datepicker as clicking outside of table - */ - this.$datePicker.on('mousedown', function (event) { - event.stopPropagation(); - }); - - this.hideDatepicker(); - }; - - DateEditor.prototype.destroyElements = function () { - this.$datePicker.datepicker('destroy'); - this.$datePicker.remove(); - }; - - DateEditor.prototype.open = function () { - Handsontable.editors.TextEditor.prototype.open.call(this); - this.showDatepicker(); - }; - - DateEditor.prototype.finishEditing = function (isCancelled, ctrlDown) { - this.hideDatepicker(); - Handsontable.editors.TextEditor.prototype.finishEditing.apply(this, arguments); - }; - - DateEditor.prototype.showDatepicker = function () { - var $td = $(this.TD); - var offset = $td.offset(); - this.datePickerStyle.top = (offset.top + $td.height()) + 'px'; - this.datePickerStyle.left = offset.left + 'px'; - - var dateOptions = { - defaultDate: this.originalValue || void 0 - }; - $.extend(dateOptions, this.cellProperties); - this.$datePicker.datepicker("option", dateOptions); - if (this.originalValue) { - this.$datePicker.datepicker("setDate", this.originalValue); - } - this.datePickerStyle.display = 'block'; - }; - - DateEditor.prototype.hideDatepicker = function () { - this.datePickerStyle.display = 'none'; - }; - - - Handsontable.editors.DateEditor = DateEditor; - Handsontable.editors.registerEditor('date', DateEditor); -})(Handsontable); -/** - * This is inception. Using Handsontable as Handsontable editor - */ -(function (Handsontable) { - "use strict"; - - var HandsontableEditor = Handsontable.editors.TextEditor.prototype.extend(); - - HandsontableEditor.prototype.createElements = function () { - Handsontable.editors.TextEditor.prototype.createElements.apply(this, arguments); - - var DIV = document.createElement('DIV'); - DIV.className = 'handsontableEditor'; - this.TEXTAREA_PARENT.appendChild(DIV); - - this.$htContainer = $(DIV); - this.$htContainer.handsontable(); - }; - - HandsontableEditor.prototype.prepare = function (td, row, col, prop, value, cellProperties) { - - Handsontable.editors.TextEditor.prototype.prepare.apply(this, arguments); - - var parent = this; - - var options = { - startRows: 0, - startCols: 0, - minRows: 0, - minCols: 0, - className: 'listbox', - copyPaste: false, - cells: function () { - return { - readOnly: true - } - }, - fillHandle: false, - afterOnCellMouseDown: function () { - var value = this.getValue(); - if (value !== void 0) { //if the value is undefined then it means we don't want to set the value - parent.setValue(value); - } - parent.instance.destroyEditor(); - }, - beforeOnKeyDown: function (event) { - var instance = this; - - switch (event.keyCode) { - case Handsontable.helper.keyCode.ESCAPE: - parent.instance.destroyEditor(true); - event.stopImmediatePropagation(); - event.preventDefault(); - break; - - case Handsontable.helper.keyCode.ENTER: //enter - var sel = instance.getSelected(); - var value = this.getDataAtCell(sel[0], sel[1]); - if (value !== void 0) { //if the value is undefined then it means we don't want to set the value - parent.setValue(value); - } - parent.instance.destroyEditor(); - break; - - case Handsontable.helper.keyCode.ARROW_UP: - if (instance.getSelected() && instance.getSelected()[0] == 0){ - instance.deselectCell(); - parent.instance.listen(); - parent.focus(); - event.preventDefault(); - event.stopImmediatePropagation(); - } - break; - } - } - }; - - if (this.cellProperties.handsontable) { - options = $.extend(options, cellProperties.handsontable); - } - this.$htContainer.handsontable('destroy'); - this.$htContainer.handsontable(options); - }; - - var onBeforeKeyDown = function (event) { - - if (event.isImmediatePropagationStopped()) { - return; - } - - var editor = this.getActiveEditor(); - var innerHOT = editor.$htContainer.handsontable('getInstance'); - - if (event.keyCode == Handsontable.helper.keyCode.ARROW_DOWN) { - - if (!innerHOT.getSelected()){ - innerHOT.selectCell(0, 0); - } else { - var selectedRow = innerHOT.getSelected()[0]; - var rowToSelect = selectedRow < innerHOT.countRows() - 1 ? selectedRow + 1 : selectedRow; - - innerHOT.selectCell(rowToSelect, 0); - } - - event.preventDefault(); - event.stopImmediatePropagation(); - } - - }; - - HandsontableEditor.prototype.open = function () { - - this.instance.addHook('beforeKeyDown', onBeforeKeyDown); - - Handsontable.editors.TextEditor.prototype.open.apply(this, arguments); - - this.$htContainer.handsontable('render'); - - if (this.cellProperties.strict) { - this.$htContainer.handsontable('selectCell', 0, 0); - this.$textarea[0].style.visibility = 'hidden'; - } else { - this.$htContainer.handsontable('deselectCell'); - this.$textarea[0].style.visibility = 'visible'; - } - - Handsontable.Dom.setCaretPosition(this.$textarea[0], 0, this.$textarea[0].value.length); - - }; - - HandsontableEditor.prototype.close = function () { - - this.instance.removeHook('beforeKeyDown', onBeforeKeyDown); - this.instance.listen(); - - Handsontable.editors.TextEditor.prototype.close.apply(this, arguments); - }; - - HandsontableEditor.prototype.focus = function () { - - this.instance.listen(); - - Handsontable.editors.TextEditor.prototype.focus.apply(this, arguments); - }; - - HandsontableEditor.prototype.beginEditing = function (initialValue) { - var onBeginEditing = this.instance.getSettings().onBeginEditing; - if (onBeginEditing && onBeginEditing() === false) { - return; - } - - Handsontable.editors.TextEditor.prototype.beginEditing.apply(this, arguments); - - }; - - HandsontableEditor.prototype.finishEditing = function (isCancelled, ctrlDown) { - if (this.$htContainer.handsontable('isListening')) { //if focus is still in the HOT editor - this.instance.listen(); //return the focus to the parent HOT instance - } - - if (this.$htContainer.handsontable('getSelected')) { - var value = this.$htContainer.handsontable('getInstance').getValue(); - if (value !== void 0) { //if the value is undefined then it means we don't want to set the value - this.setValue(value); - } - } - - return Handsontable.editors.TextEditor.prototype.finishEditing.apply(this, arguments); - }; - - Handsontable.editors.HandsontableEditor = HandsontableEditor; - Handsontable.editors.registerEditor('handsontable', HandsontableEditor); - -})(Handsontable); - - - - - - -(function (Handsontable) { - var AutocompleteEditor = Handsontable.editors.HandsontableEditor.prototype.extend(); - - AutocompleteEditor.prototype.init = function () { - Handsontable.editors.HandsontableEditor.prototype.init.apply(this, arguments); - this.$htContainer.handsontable('updateSettings', {height: this.getDropdownHeight()}); - - this.query = null; - this.choices = []; - }; - - AutocompleteEditor.prototype.createElements = function(){ - Handsontable.editors.HandsontableEditor.prototype.createElements.apply(this, arguments); - - this.$htContainer.addClass('autocompleteEditor'); - - }; - - AutocompleteEditor.prototype.bindEvents = function(){ - var that = this; - - this.$textarea.on('keydown.autocompleteEditor', function(event){ - - var value = that.$textarea.val(); - - if(!Handsontable.helper.isMetaKey(event.keyCode) || [Handsontable.helper.keyCode.BACKSPACE, Handsontable.helper.keyCode.DELETE].indexOf(event.keyCode) !== -1){ - this.instance._registerTimeout(setTimeout(function () { - that.queryChoices(value); - }, 0)); - } else if ([Handsontable.helper.keyCode.ENTER, Handsontable.helper.keyCode.TAB].indexOf(event.keyCode) !== -1){ - - var choice = that.choices[0]; - - if (value.length > 0 && choice) { - if (choice.length > 0) { - that.$textarea[0].value = choice; - } - } - - if (that.cellProperties.strict !== true) { - that.$htContainer.handsontable('deselectCell'); - } - } - - }); - - this.$htContainer.on('mouseenter', function () { - that.$htContainer.handsontable('deselectCell'); - }); - - Handsontable.editors.HandsontableEditor.prototype.bindEvents.apply(this, arguments); - - }; - - var onBeforeKeyDownInner; - - AutocompleteEditor.prototype.open = function () { - - Handsontable.editors.HandsontableEditor.prototype.open.apply(this, arguments); - - this.$textarea[0].style.visibility = 'visible'; - this.focus(); - - var choicesListHot = this.$htContainer.handsontable('getInstance'); - var that = this; - - choicesListHot.updateSettings({ - 'colWidths': [Handsontable.Dom.outerWidth(this.TEXTAREA) - 2], - afterRenderer: function (TD, row, col, prop, value) { - var caseSensitive = this.getCellMeta(row, col).filteringCaseSensitive === true; - var indexOfMatch = caseSensitive ? value.indexOf(this.query) : value.toLowerCase().indexOf(that.query.toLowerCase()); - - if(indexOfMatch != -1){ - var match = value.substr(indexOfMatch, that.query.length); - TD.innerHTML = value.replace(match, '' + match + ''); - } - } - }); - - onBeforeKeyDownInner = function (event) { - var instance = this; - - if (event.keyCode == Handsontable.helper.keyCode.ARROW_UP){ - if (instance.getSelected() && instance.getSelected()[0] == 0){ - - var cellProperties = parent.cellProperties; - - if (cellProperties && !cellProperties.strict) { - instance.deselectCell(); - } - - if (parent) { - parent.focus(); - - if (parent.instance) { - parent.instance.listen(); - } else { - instance.listen(); - } - - } - - event.preventDefault(); - event.stopImmediatePropagation(); - } - } - - }; - - choicesListHot.addHook('beforeKeyDown', onBeforeKeyDownInner); - - this.queryChoices(this.TEXTAREA.value); - - }; - - AutocompleteEditor.prototype.close = function () { - - this.$htContainer.handsontable('getInstance').removeHook('beforeKeyDown', onBeforeKeyDownInner); - - Handsontable.editors.HandsontableEditor.prototype.close.apply(this, arguments); - }; - - AutocompleteEditor.prototype.queryChoices = function(query){ - this.query = query; - - if (typeof this.cellProperties.source == 'function'){ - var that = this; - - this.cellProperties.source(query, function(choices){ - that.updateChoicesList(choices) - }); - - } else if (Handsontable.helper.isArray(this.cellProperties.source)) { - - var choices; - - if(!query || this.cellProperties.filter === false){ - choices = this.cellProperties.source; - } else { - - var filteringCaseSensitive = this.cellProperties.filteringCaseSensitive === true; - var lowerCaseQuery = query.toLowerCase(); - - choices = this.cellProperties.source.filter(function(choice){ - - if (filteringCaseSensitive) { - return choice.indexOf(query) != -1; - } else { - return choice.toLowerCase().indexOf(lowerCaseQuery) != -1; - } - - }); - } - - this.updateChoicesList(choices) - - } else { - this.updateChoicesList([]); - } - - }; - - AutocompleteEditor.prototype.updateChoicesList = function (choices) { - - this.choices = choices; - - this.$htContainer.handsontable('loadData', Handsontable.helper.pivot([choices])); - - //if(this.cellProperties.strict === true) { - // this.highlightBestMatchingChoice(); - //} - - //this.focus(); // this override textEditor events ie. ctrl combinations - // Can't highlight text in the cell with ctrl+a in autocomplete demo #1590 - // Editing autocomplete selection, cursor goes to the end of selected string. #1610 - }; - - AutocompleteEditor.prototype.highlightBestMatchingChoice = function () { - var bestMatchingChoice = this.findBestMatchingChoice(); - - if ( typeof bestMatchingChoice == 'undefined' && this.cellProperties.allowInvalid === false){ - bestMatchingChoice = 0; - } - - if(typeof bestMatchingChoice == 'undefined'){ - this.$htContainer.handsontable('deselectCell'); - } else { - this.$htContainer.handsontable('selectCell', bestMatchingChoice, 0); - } - - }; - - AutocompleteEditor.prototype.findBestMatchingChoice = function(){ - var bestMatch = {}; - var valueLength = this.getValue().length; - var currentItem; - var indexOfValue; - var charsLeft; - - - for(var i = 0, len = this.choices.length; i < len; i++){ - currentItem = this.choices[i]; - - if(valueLength > 0){ - indexOfValue = currentItem.indexOf(this.getValue()) - } else { - indexOfValue = currentItem === this.getValue() ? 0 : -1; - } - - if(indexOfValue == -1) continue; - - charsLeft = currentItem.length - indexOfValue - valueLength; - - if( typeof bestMatch.indexOfValue == 'undefined' - || bestMatch.indexOfValue > indexOfValue - || ( bestMatch.indexOfValue == indexOfValue && bestMatch.charsLeft > charsLeft ) ){ - - bestMatch.indexOfValue = indexOfValue; - bestMatch.charsLeft = charsLeft; - bestMatch.index = i; - - } - - } - - - return bestMatch.index; - }; - - AutocompleteEditor.prototype.getDropdownHeight = function(){ - //return 10 * this.$htContainer.handsontable('getInstance').getRowHeight(0); - //sorry, we can't measure row height before it was rendered. Let's use fixed height for now - return 230; - }; - - - Handsontable.editors.AutocompleteEditor = AutocompleteEditor; - Handsontable.editors.registerEditor('autocomplete', AutocompleteEditor); - -})(Handsontable); - -(function(Handsontable){ - - var PasswordEditor = Handsontable.editors.TextEditor.prototype.extend(); - - PasswordEditor.prototype.createElements = function () { - Handsontable.editors.TextEditor.prototype.createElements.apply(this, arguments); - - this.TEXTAREA = document.createElement('input'); - this.TEXTAREA.setAttribute('type', 'password'); - this.TEXTAREA.className = 'handsontableInput'; - this.textareaStyle = this.TEXTAREA.style; - this.textareaStyle.width = 0; - this.textareaStyle.height = 0; - this.$textarea = $(this.TEXTAREA); - - Handsontable.Dom.empty(this.TEXTAREA_PARENT); - this.TEXTAREA_PARENT.appendChild(this.TEXTAREA); - - }; - - Handsontable.editors.PasswordEditor = PasswordEditor; - Handsontable.editors.registerEditor('password', PasswordEditor); - -})(Handsontable); - -(function (Handsontable) { - - var SelectEditor = Handsontable.editors.BaseEditor.prototype.extend(); - - SelectEditor.prototype.init = function(){ - this.select = document.createElement('SELECT'); - Handsontable.Dom.addClass(this.select, 'htSelectEditor'); - this.select.style.display = 'none'; - this.instance.rootElement[0].appendChild(this.select); - }; - - SelectEditor.prototype.prepare = function(){ - Handsontable.editors.BaseEditor.prototype.prepare.apply(this, arguments); - - - var selectOptions = this.cellProperties.selectOptions; - var options; - - if (typeof selectOptions == 'function'){ - options = this.prepareOptions(selectOptions(this.row, this.col, this.prop)) - } else { - options = this.prepareOptions(selectOptions); - } - - Handsontable.Dom.empty(this.select); - - for (var option in options){ - if (options.hasOwnProperty(option)){ - var optionElement = document.createElement('OPTION'); - optionElement.value = option; - Handsontable.Dom.fastInnerHTML(optionElement, options[option]); - this.select.appendChild(optionElement); - } - } - }; - - SelectEditor.prototype.prepareOptions = function(optionsToPrepare){ - - var preparedOptions = {}; - - if (Handsontable.helper.isArray(optionsToPrepare)){ - for(var i = 0, len = optionsToPrepare.length; i < len; i++){ - preparedOptions[optionsToPrepare[i]] = optionsToPrepare[i]; - } - } - else if (typeof optionsToPrepare == 'object') { - preparedOptions = optionsToPrepare; - } - - return preparedOptions; - - }; - - SelectEditor.prototype.getValue = function () { - return this.select.value; - }; - - SelectEditor.prototype.setValue = function (value) { - this.select.value = value; - }; - - var onBeforeKeyDown = function (event) { - var instance = this; - var editor = instance.getActiveEditor(); - - switch (event.keyCode){ - case Handsontable.helper.keyCode.ARROW_UP: - - var previousOption = editor.select.find('option:selected').prev(); - - if (previousOption.length == 1){ - previousOption.prop('selected', true); - } - - event.stopImmediatePropagation(); - event.preventDefault(); - break; - - case Handsontable.helper.keyCode.ARROW_DOWN: - - var nextOption = editor.select.find('option:selected').next(); - - if (nextOption.length == 1){ - nextOption.prop('selected', true); - } - - event.stopImmediatePropagation(); - event.preventDefault(); - break; - } - }; - - SelectEditor.prototype.open = function () { - var width = Handsontable.Dom.outerWidth(this.TD); //important - group layout reads together for better performance - var height = Handsontable.Dom.outerHeight(this.TD); - var rootOffset = Handsontable.Dom.offset(this.instance.rootElement[0]); - var tdOffset = Handsontable.Dom.offset(this.TD); - - this.select.style.height = height + 'px'; - this.select.style.minWidth = width + 'px'; - this.select.style.top = tdOffset.top - rootOffset.top + 'px'; - this.select.style.left = tdOffset.left - rootOffset.left + 'px'; - this.select.style.margin = '0px'; - this.select.style.display = ''; - - this.instance.addHook('beforeKeyDown', onBeforeKeyDown); - }; - - SelectEditor.prototype.close = function () { - this.select.style.display = 'none'; - this.instance.removeHook('beforeKeyDown', onBeforeKeyDown); - }; - - SelectEditor.prototype.focus = function () { - this.select.focus(); - }; - - Handsontable.editors.SelectEditor = SelectEditor; - Handsontable.editors.registerEditor('select', SelectEditor); - -})(Handsontable); - -(function (Handsontable) { - - var DropdownEditor = Handsontable.editors.AutocompleteEditor.prototype.extend(); - - DropdownEditor.prototype.prepare = function () { - Handsontable.editors.AutocompleteEditor.prototype.prepare.apply(this, arguments); - - this.cellProperties.filter = false; - this.cellProperties.strict = true; - - }; - - - Handsontable.editors.DropdownEditor = DropdownEditor; - Handsontable.editors.registerEditor('dropdown', DropdownEditor); - - -})(Handsontable); -/** - * Numeric cell validator - * @param {*} value - Value of edited cell - * @param {*} callback - Callback called with validation result - */ -Handsontable.NumericValidator = function (value, callback) { - if (value === null) { - value = ''; - } - callback(/^-?\d*(\.|\,)?\d*$/.test(value)); -}; -/** - * Function responsible for validation of autocomplete value - * @param {*} value - Value of edited cell - * @param {*} calback - Callback called with validation result - */ -var process = function (value, callback) { - - var originalVal = value; - var lowercaseVal = typeof originalVal === 'string' ? originalVal.toLowerCase() : null; - - return function (source) { - var found = false; - for (var s = 0, slen = source.length; s < slen; s++) { - if (originalVal === source[s]) { - found = true; //perfect match - break; - } - else if (lowercaseVal === source[s].toLowerCase()) { - // changes[i][3] = source[s]; //good match, fix the case << TODO? - found = true; - break; - } - } - - callback(found); - } -}; - -/** - * Autocomplete cell validator - * @param {*} value - Value of edited cell - * @param {*} calback - Callback called with validation result - */ -Handsontable.AutocompleteValidator = function (value, callback) { - if (this.strict && this.source) { - typeof this.source === 'function' ? this.source(value, process(value, callback)) : process(value, callback)(this.source); - } else { - callback(true); - } -}; - -/** - * Cell type is just a shortcut for setting bunch of cellProperties (used in getCellMeta) - */ - -Handsontable.AutocompleteCell = { - editor: Handsontable.editors.AutocompleteEditor, - renderer: Handsontable.renderers.AutocompleteRenderer, - validator: Handsontable.AutocompleteValidator -}; - -Handsontable.CheckboxCell = { - editor: Handsontable.editors.CheckboxEditor, - renderer: Handsontable.renderers.CheckboxRenderer -}; - -Handsontable.TextCell = { - editor: Handsontable.editors.TextEditor, - renderer: Handsontable.renderers.TextRenderer -}; - -Handsontable.NumericCell = { - editor: Handsontable.editors.TextEditor, - renderer: Handsontable.renderers.NumericRenderer, - validator: Handsontable.NumericValidator, - dataType: 'number' -}; - -Handsontable.DateCell = { - editor: Handsontable.editors.DateEditor, - renderer: Handsontable.renderers.AutocompleteRenderer //displays small gray arrow on right side of the cell -}; - -Handsontable.HandsontableCell = { - editor: Handsontable.editors.HandsontableEditor, - renderer: Handsontable.renderers.AutocompleteRenderer //displays small gray arrow on right side of the cell -}; - -Handsontable.PasswordCell = { - editor: Handsontable.editors.PasswordEditor, - renderer: Handsontable.renderers.PasswordRenderer, - copyable: false -}; - -Handsontable.DropdownCell = { - editor: Handsontable.editors.DropdownEditor, - renderer: Handsontable.renderers.AutocompleteRenderer, //displays small gray arrow on right side of the cell - validator: Handsontable.AutocompleteValidator -}; - -//here setup the friendly aliases that are used by cellProperties.type -Handsontable.cellTypes = { - text: Handsontable.TextCell, - date: Handsontable.DateCell, - numeric: Handsontable.NumericCell, - checkbox: Handsontable.CheckboxCell, - autocomplete: Handsontable.AutocompleteCell, - handsontable: Handsontable.HandsontableCell, - password: Handsontable.PasswordCell, - dropdown: Handsontable.DropdownCell -}; - -//here setup the friendly aliases that are used by cellProperties.renderer and cellProperties.editor -Handsontable.cellLookup = { - validator: { - numeric: Handsontable.NumericValidator, - autocomplete: Handsontable.AutocompleteValidator - } -}; -/** - * autoResize - resizes a DOM element to the width and height of another DOM element - * - * Copyright 2014, Marcin Warpechowski - * Licensed under the MIT license - */ -var autoResize = function () { - var defaults = { - minHeight: 200, - maxHeight: 300, - minWidth: 100, - maxWidth: 300, - fontSize: 11 - }, - el, - body = document.body, - text = document.createTextNode(''), - span = document.createElement('SPAN'), - observe = function (element, event, handler) { - if (window.attachEvent) { - element.attachEvent('on' + event, handler); - } else { - element.addEventListener(event, handler, false); - } - }, - unObserve = function (element, event, handler) { - if (window.detachEvent) { - element.detachEvent('on' + event, handler); - } else { - element.removeEventListener(event, handler, false); - } - }, - resize = function () { - if (text.textContent !== void 0) { - text.textContent = el.value; - } - else { - text.data = el.value; //IE8 - } - span.style.fontSize = defaults.fontSize + 'px'; - - body.appendChild(span); - var width = span.clientWidth; - body.removeChild(span); - - el.style.height = defaults.minHeight + 'px'; - - if (defaults.minWidth > width) { - el.style.width = defaults.minWidth + 'px'; - } else if (width > defaults.maxWidth) { - el.style.width = defaults.maxWidth + 'px'; - } else { - el.style.width = width + 'px'; - } - - var scrollHeight = el.scrollHeight; - if (defaults.minHeight > scrollHeight) { - el.style.height = defaults.minHeight + 'px'; - } else if (defaults.maxHeight < scrollHeight) { - el.style.height = defaults.maxHeight + 'px'; - el.style.overflowY = 'visible'; - } else { - el.style.height = scrollHeight + 'px'; - } - - }, - delayedResize = function () { - window.setTimeout(resize, 0); - }, - extendDefaults = function (config) { - - if (config && config.minHeight) { - if (config.minHeight == 'inherit') { - defaults.minHeight = el.clientHeight; - } else { - var minHeight = parseInt(config.minHeight); - if (!isNaN(minHeight)) { - defaults.minHeight = minHeight - } - } - } - - if (config && config.maxHeight) { - if (config.maxHeight == 'inherit') { - defaults.maxHeight = el.clientHeight; - } else { - var maxHeight = parseInt(config.maxHeight); - if (!isNaN(maxHeight)) { - defaults.maxHeight = maxHeight - } - } - } - - if (config && config.minWidth) { - if (config.minWidth == 'inherit') { - defaults.minWidth = el.clientWidth; - } else { - var minWidth = parseInt(config.minWidth); - if (!isNaN(minWidth)) { - defaults.minWidth = minWidth - } - } - } - - if (config && config.maxWidth) { - if (config.maxWidth == 'inherit') { - defaults.maxWidth = el.clientWidth; - } else { - var maxWidth = parseInt(config.maxWidth); - if (!isNaN(maxWidth)) { - defaults.maxWidth = maxWidth - } - } - } - - if (config && config.fontSize) { - if (config.fontSize == 'inherit') { - defaults.fontSize = el.fontSize; - } else { - var fontSize = parseInt(config.fontSize); - if (!isNaN(fontSize)) { - defaults.fontSize = fontSize - } - } - } - - if(!span.firstChild) { - span.className = "autoResize"; - span.style.display = 'inline-block'; - span.appendChild(text); - } - }, - init = function (el_, config) { - el = el_; - extendDefaults(config); - - if (el.nodeName == 'TEXTAREA') { - - el.style.resize = 'none'; - el.style.overflowY = ''; - el.style.height = defaults.minHeight + 'px'; - el.style.minWidth = defaults.minWidth + 'px'; - el.style.maxWidth = defaults.maxWidth + 'px'; - el.style.fontSize = defaults.fontSize + 'px'; - el.style.overflowY = 'hidden'; - } - - - observe(el, 'change', resize); - observe(el, 'cut', delayedResize); - observe(el, 'paste', delayedResize); - observe(el, 'drop', delayedResize); - observe(el, 'keydown', delayedResize); - - resize(); - }; - - return { - init: function (el_, config) { - init(el_, config); - }, - unObserve: function () { - unObserve(el, 'change', resize); - unObserve(el, 'cut', delayedResize); - unObserve(el, 'paste', delayedResize); - unObserve(el, 'drop', delayedResize); - unObserve(el, 'keydown', delayedResize); - } - } - -}; - -/** - * SheetClip - Spreadsheet Clipboard Parser - * version 0.2 - * - * This tiny library transforms JavaScript arrays to strings that are pasteable by LibreOffice, OpenOffice, - * Google Docs and Microsoft Excel. - * - * Copyright 2012, Marcin Warpechowski - * Licensed under the MIT license. - * http://github.com/warpech/sheetclip/ - */ -/*jslint white: true*/ -(function (global) { - "use strict"; - - function countQuotes(str) { - return str.split('"').length - 1; - } - - global.SheetClip = { - parse: function (str) { - var r, rlen, rows, arr = [], a = 0, c, clen, multiline, last; - rows = str.split('\n'); - if (rows.length > 1 && rows[rows.length - 1] === '') { - rows.pop(); - } - for (r = 0, rlen = rows.length; r < rlen; r += 1) { - rows[r] = rows[r].split('\t'); - for (c = 0, clen = rows[r].length; c < clen; c += 1) { - if (!arr[a]) { - arr[a] = []; - } - if (multiline && c === 0) { - last = arr[a].length - 1; - arr[a][last] = arr[a][last] + '\n' + rows[r][0]; - if (multiline && (countQuotes(rows[r][0]) & 1)) { //& 1 is a bitwise way of performing mod 2 - multiline = false; - arr[a][last] = arr[a][last].substring(0, arr[a][last].length - 1).replace(/""/g, '"'); - } - } - else { - if (c === clen - 1 && rows[r][c].indexOf('"') === 0) { - arr[a].push(rows[r][c].substring(1).replace(/""/g, '"')); - multiline = true; - } - else { - arr[a].push(rows[r][c].replace(/""/g, '"')); - multiline = false; - } - } - } - if (!multiline) { - a += 1; - } - } - return arr; - }, - - stringify: function (arr) { - var r, rlen, c, clen, str = '', val; - for (r = 0, rlen = arr.length; r < rlen; r += 1) { - for (c = 0, clen = arr[r].length; c < clen; c += 1) { - if (c > 0) { - str += '\t'; - } - val = arr[r][c]; - if (typeof val === 'string') { - if (val.indexOf('\n') > -1) { - str += '"' + val.replace(/"/g, '""') + '"'; - } - else { - str += val; - } - } - else if (val === null || val === void 0) { //void 0 resolves to undefined - str += ''; - } - else { - str += val; - } - } - str += '\n'; - } - return str; - } - }; -}(window)); -/** - * CopyPaste.js - * Creates a textarea that stays hidden on the page and gets focused when user presses CTRL while not having a form input focused - * In future we may implement a better driver when better APIs are available - * @constructor - */ -var CopyPaste = (function () { - var instance; - return { - getInstance: function () { - if (!instance) { - instance = new CopyPasteClass(); - } else if (instance.hasBeenDestroyed()){ - instance.init(); - } - - instance.refCounter++; - - return instance; - } - }; -})(); - -function CopyPasteClass() { - this.refCounter = 0; - this.init(); -} - -CopyPasteClass.prototype.init = function () { - var that = this - , style - , parent; - - this.copyCallbacks = []; - this.cutCallbacks = []; - this.pasteCallbacks = []; - - this.listenerElement = document.documentElement; - parent = document.body; - - if (document.getElementById('CopyPasteDiv')) { - this.elDiv = document.getElementById('CopyPasteDiv'); - this.elTextarea = this.elDiv.firstChild; - } - else { - this.elDiv = document.createElement('DIV'); - this.elDiv.id = 'CopyPasteDiv'; - style = this.elDiv.style; - style.position = 'fixed'; - style.top = '-10000px'; - style.left = '-10000px'; - parent.appendChild(this.elDiv); - - this.elTextarea = document.createElement('TEXTAREA'); - this.elTextarea.className = 'copyPaste'; - style = this.elTextarea.style; - style.width = '10000px'; - style.height = '10000px'; - style.overflow = 'hidden'; - this.elDiv.appendChild(this.elTextarea); - - if (typeof style.opacity !== 'undefined') { - style.opacity = 0; - } - else { - /*@cc_on @if (@_jscript) - if(typeof style.filter === 'string') { - style.filter = 'alpha(opacity=0)'; - } - @end @*/ - } - } - - this.keydownListener = function (event) { - var isCtrlDown = false; - if (event.metaKey) { //mac - isCtrlDown = true; - } - else if (event.ctrlKey && navigator.userAgent.indexOf('Mac') === -1) { //pc - isCtrlDown = true; - } - - if (isCtrlDown) { - if (document.activeElement !== that.elTextarea && (that.getSelectionText() != '' || ['INPUT', 'SELECT', 'TEXTAREA'].indexOf(document.activeElement.nodeName) != -1)) { - return; //this is needed by fragmentSelection in Handsontable. Ignore copypaste.js behavior if fragment of cell text is selected - } - - that.selectNodeText(that.elTextarea); - setTimeout(function () { - that.selectNodeText(that.elTextarea); - }, 0); - } - - /* 67 = c - * 86 = v - * 88 = x - */ - if (isCtrlDown && (event.keyCode === 67 || event.keyCode === 86 || event.keyCode === 88)) { - // that.selectNodeText(that.elTextarea); - - if (event.keyCode === 88) { //works in all browsers, incl. Opera < 12.12 - setTimeout(function () { - that.triggerCut(event); - }, 0); - } - else if (event.keyCode === 86) { - setTimeout(function () { - that.triggerPaste(event); - }, 0); - } - } - } - - this._bindEvent(this.listenerElement, 'keydown', this.keydownListener); -}; - -//http://jsperf.com/textara-selection -//http://stackoverflow.com/questions/1502385/how-can-i-make-this-code-work-in-ie -CopyPasteClass.prototype.selectNodeText = function (el) { - el.select(); -}; - -//http://stackoverflow.com/questions/5379120/get-the-highlighted-selected-text -CopyPasteClass.prototype.getSelectionText = function () { - var text = ""; - if (window.getSelection) { - text = window.getSelection().toString(); - } else if (document.selection && document.selection.type != "Control") { - text = document.selection.createRange().text; - } - return text; -}; - -CopyPasteClass.prototype.copyable = function (str) { - if (typeof str !== 'string' && str.toString === void 0) { - throw new Error('copyable requires string parameter'); - } - this.elTextarea.value = str; -}; - -/*CopyPasteClass.prototype.onCopy = function (fn) { - this.copyCallbacks.push(fn); -};*/ - -CopyPasteClass.prototype.onCut = function (fn) { - this.cutCallbacks.push(fn); -}; - -CopyPasteClass.prototype.onPaste = function (fn) { - this.pasteCallbacks.push(fn); -}; - -CopyPasteClass.prototype.removeCallback = function (fn) { - var i, ilen; - for (i = 0, ilen = this.copyCallbacks.length; i < ilen; i++) { - if (this.copyCallbacks[i] === fn) { - this.copyCallbacks.splice(i, 1); - return true; - } - } - for (i = 0, ilen = this.cutCallbacks.length; i < ilen; i++) { - if (this.cutCallbacks[i] === fn) { - this.cutCallbacks.splice(i, 1); - return true; - } - } - for (i = 0, ilen = this.pasteCallbacks.length; i < ilen; i++) { - if (this.pasteCallbacks[i] === fn) { - this.pasteCallbacks.splice(i, 1); - return true; - } - } - return false; -}; - -CopyPasteClass.prototype.triggerCut = function (event) { - var that = this; - if (that.cutCallbacks) { - setTimeout(function () { - for (var i = 0, ilen = that.cutCallbacks.length; i < ilen; i++) { - that.cutCallbacks[i](event); - } - }, 50); - } -}; - -CopyPasteClass.prototype.triggerPaste = function (event, str) { - var that = this; - if (that.pasteCallbacks) { - setTimeout(function () { - var val = (str || that.elTextarea.value).replace(/\n$/, ''); //remove trailing newline - for (var i = 0, ilen = that.pasteCallbacks.length; i < ilen; i++) { - that.pasteCallbacks[i](val, event); - } - }, 50); - } -}; - -CopyPasteClass.prototype.destroy = function () { - - if(!this.hasBeenDestroyed() && --this.refCounter == 0){ - if (this.elDiv && this.elDiv.parentNode) { - this.elDiv.parentNode.removeChild(this.elDiv); - this.elDiv = null; - this.elTextarea = null; - } - - this._unbindEvent(this.listenerElement, 'keydown', this.keydownListener); - - } - -}; - -CopyPasteClass.prototype.hasBeenDestroyed = function () { - return !this.refCounter; -}; - -//old version used this: -// - http://net.tutsplus.com/tutorials/javascript-ajax/javascript-from-null-cross-browser-event-binding/ -// - http://stackoverflow.com/questions/4643249/cross-browser-event-object-normalization -//but that cannot work with jQuery.trigger -CopyPasteClass.prototype._bindEvent = (function () { - if (window.jQuery) { //if jQuery exists, use jQuery event (for compatibility with $.trigger and $.triggerHandler, which can only trigger jQuery events - and we use that in tests) - return function (elem, type, cb) { - $(elem).on(type + '.copypaste', cb); - }; - } - else { - return function (elem, type, cb) { - elem.addEventListener(type, cb, false); //sorry, IE8 will only work with jQuery - }; - } -})(); - -CopyPasteClass.prototype._unbindEvent = (function () { - if (window.jQuery) { //if jQuery exists, use jQuery event (for compatibility with $.trigger and $.triggerHandler, which can only trigger jQuery events - and we use that in tests) - return function (elem, type, cb) { - $(elem).off(type + '.copypaste', cb); - }; - } - else { - return function (elem, type, cb) { - elem.removeEventListener(type, cb, false); //sorry, IE8 will only work with jQuery - }; - } -})(); -// json-patch-duplex.js 0.3.6 -// (c) 2013 Joachim Wester -// MIT license -var jsonpatch; -(function (jsonpatch) { - var objOps = { - add: function (obj, key) { - obj[key] = this.value; - return true; - }, - remove: function (obj, key) { - delete obj[key]; - return true; - }, - replace: function (obj, key) { - obj[key] = this.value; - return true; - }, - move: function (obj, key, tree) { - var temp = { op: "_get", path: this.from }; - apply(tree, [temp]); - apply(tree, [ - { op: "remove", path: this.from } - ]); - apply(tree, [ - { op: "add", path: this.path, value: temp.value } - ]); - return true; - }, - copy: function (obj, key, tree) { - var temp = { op: "_get", path: this.from }; - apply(tree, [temp]); - apply(tree, [ - { op: "add", path: this.path, value: temp.value } - ]); - return true; - }, - test: function (obj, key) { - return (JSON.stringify(obj[key]) === JSON.stringify(this.value)); - }, - _get: function (obj, key) { - this.value = obj[key]; - } - }; - - var arrOps = { - add: function (arr, i) { - arr.splice(i, 0, this.value); - return true; - }, - remove: function (arr, i) { - arr.splice(i, 1); - return true; - }, - replace: function (arr, i) { - arr[i] = this.value; - return true; - }, - move: objOps.move, - copy: objOps.copy, - test: objOps.test, - _get: objOps._get - }; - - var observeOps = { - add: function (patches, path) { - var patch = { - op: "add", - path: path + escapePathComponent(this.name), - value: this.object[this.name] - }; - patches.push(patch); - }, - 'delete': function (patches, path) { - var patch = { - op: "remove", - path: path + escapePathComponent(this.name) - }; - patches.push(patch); - }, - update: function (patches, path) { - var patch = { - op: "replace", - path: path + escapePathComponent(this.name), - value: this.object[this.name] - }; - patches.push(patch); - } - }; - - function escapePathComponent(str) { - if (str.indexOf('/') === -1 && str.indexOf('~') === -1) - return str; - return str.replace(/~/g, '~0').replace(/\//g, '~1'); - } - - function _getPathRecursive(root, obj) { - var found; - for (var key in root) { - if (root.hasOwnProperty(key)) { - if (root[key] === obj) { - return escapePathComponent(key) + '/'; - } else if (typeof root[key] === 'object') { - found = _getPathRecursive(root[key], obj); - if (found != '') { - return escapePathComponent(key) + '/' + found; - } - } - } - } - return ''; - } - - function getPath(root, obj) { - if (root === obj) { - return '/'; - } - var path = _getPathRecursive(root, obj); - if (path === '') { - throw new Error("Object not found in root"); - } - return '/' + path; - } - - var beforeDict = []; - - jsonpatch.intervals; - - var Mirror = (function () { - function Mirror(obj) { - this.observers = []; - this.obj = obj; - } - return Mirror; - })(); - - var ObserverInfo = (function () { - function ObserverInfo(callback, observer) { - this.callback = callback; - this.observer = observer; - } - return ObserverInfo; - })(); - - function getMirror(obj) { - for (var i = 0, ilen = beforeDict.length; i < ilen; i++) { - if (beforeDict[i].obj === obj) { - return beforeDict[i]; - } - } - } - - function getObserverFromMirror(mirror, callback) { - for (var j = 0, jlen = mirror.observers.length; j < jlen; j++) { - if (mirror.observers[j].callback === callback) { - return mirror.observers[j].observer; - } - } - } - - function removeObserverFromMirror(mirror, observer) { - for (var j = 0, jlen = mirror.observers.length; j < jlen; j++) { - if (mirror.observers[j].observer === observer) { - mirror.observers.splice(j, 1); - return; - } - } - } - - function unobserve(root, observer) { - generate(observer); - if (Object.observe) { - _unobserve(observer, root); - } else { - clearTimeout(observer.next); - } - - var mirror = getMirror(root); - removeObserverFromMirror(mirror, observer); - } - jsonpatch.unobserve = unobserve; - - function observe(obj, callback) { - var patches = []; - var root = obj; - var observer; - var mirror = getMirror(obj); - - if (!mirror) { - mirror = new Mirror(obj); - beforeDict.push(mirror); - } else { - observer = getObserverFromMirror(mirror, callback); - } - - if (observer) { - return observer; - } - - if (Object.observe) { - observer = function (arr) { - //This "refresh" is needed to begin observing new object properties - _unobserve(observer, obj); - _observe(observer, obj); - - var a = 0, alen = arr.length; - while (a < alen) { - if (!(arr[a].name === 'length' && _isArray(arr[a].object)) && !(arr[a].name === '__Jasmine_been_here_before__')) { - var type = arr[a].type; - - switch (type) { - case 'new': - type = 'add'; - break; - - case 'deleted': - type = 'delete'; - break; - - case 'updated': - type = 'update'; - break; - } - - observeOps[type].call(arr[a], patches, getPath(root, arr[a].object)); - } - a++; - } - - if (patches) { - if (callback) { - callback(patches); - } - } - observer.patches = patches; - patches = []; - }; - } else { - observer = {}; - - mirror.value = JSON.parse(JSON.stringify(obj)); - - if (callback) { - //callbacks.push(callback); this has no purpose - observer.callback = callback; - observer.next = null; - var intervals = this.intervals || [100, 1000, 10000, 60000]; - var currentInterval = 0; - - var dirtyCheck = function () { - generate(observer); - }; - var fastCheck = function () { - clearTimeout(observer.next); - observer.next = setTimeout(function () { - dirtyCheck(); - currentInterval = 0; - observer.next = setTimeout(slowCheck, intervals[currentInterval++]); - }, 0); - }; - var slowCheck = function () { - dirtyCheck(); - if (currentInterval == intervals.length) - currentInterval = intervals.length - 1; - observer.next = setTimeout(slowCheck, intervals[currentInterval++]); - }; - if (typeof window !== 'undefined') { - if (window.addEventListener) { - window.addEventListener('mousedown', fastCheck); - window.addEventListener('mouseup', fastCheck); - window.addEventListener('keydown', fastCheck); - } else { - window.attachEvent('onmousedown', fastCheck); - window.attachEvent('onmouseup', fastCheck); - window.attachEvent('onkeydown', fastCheck); - } - } - observer.next = setTimeout(slowCheck, intervals[currentInterval++]); - } - } - observer.patches = patches; - observer.object = obj; - - mirror.observers.push(new ObserverInfo(callback, observer)); - - return _observe(observer, obj); - } - jsonpatch.observe = observe; - - /// Listen to changes on an object tree, accumulate patches - function _observe(observer, obj) { - if (Object.observe) { - Object.observe(obj, observer); - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - var v = obj[key]; - if (v && typeof (v) === "object") { - _observe(observer, v); - } - } - } - } - return observer; - } - - function _unobserve(observer, obj) { - if (Object.observe) { - Object.unobserve(obj, observer); - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - var v = obj[key]; - if (v && typeof (v) === "object") { - _unobserve(observer, v); - } - } - } - } - return observer; - } - - function generate(observer) { - if (Object.observe) { - Object.deliverChangeRecords(observer); - } else { - var mirror; - for (var i = 0, ilen = beforeDict.length; i < ilen; i++) { - if (beforeDict[i].obj === observer.object) { - mirror = beforeDict[i]; - break; - } - } - _generate(mirror.value, observer.object, observer.patches, ""); - } - var temp = observer.patches; - if (temp.length > 0) { - observer.patches = []; - if (observer.callback) { - observer.callback(temp); - } - } - return temp; - } - jsonpatch.generate = generate; - - var _objectKeys; - if (Object.keys) { - _objectKeys = Object.keys; - } else { - _objectKeys = function (obj) { - var keys = []; - for (var o in obj) { - if (obj.hasOwnProperty(o)) { - keys.push(o); - } - } - return keys; - }; - } - - // Dirty check if obj is different from mirror, generate patches and update mirror - function _generate(mirror, obj, patches, path) { - var newKeys = _objectKeys(obj); - var oldKeys = _objectKeys(mirror); - var changed = false; - var deleted = false; - - for (var t = oldKeys.length - 1; t >= 0; t--) { - var key = oldKeys[t]; - var oldVal = mirror[key]; - if (obj.hasOwnProperty(key)) { - var newVal = obj[key]; - if (oldVal instanceof Object) { - _generate(oldVal, newVal, patches, path + "/" + escapePathComponent(key)); - } else { - if (oldVal != newVal) { - changed = true; - patches.push({ op: "replace", path: path + "/" + escapePathComponent(key), value: newVal }); - mirror[key] = newVal; - } - } - } else { - patches.push({ op: "remove", path: path + "/" + escapePathComponent(key) }); - delete mirror[key]; - deleted = true; - } - } - - if (!deleted && newKeys.length == oldKeys.length) { - return; - } - - for (var t = 0; t < newKeys.length; t++) { - var key = newKeys[t]; - if (!mirror.hasOwnProperty(key)) { - patches.push({ op: "add", path: path + "/" + escapePathComponent(key), value: obj[key] }); - mirror[key] = JSON.parse(JSON.stringify(obj[key])); - } - } - } - - var _isArray; - if (Array.isArray) { - _isArray = Array.isArray; - } else { - _isArray = function (obj) { - return obj.push && typeof obj.length === 'number'; - }; - } - - /// Apply a json-patch operation on an object tree - function apply(tree, patches) { - var result = false, p = 0, plen = patches.length, patch; - while (p < plen) { - patch = patches[p]; - - // Find the object - var keys = patch.path.split('/'); - var obj = tree; - var t = 1; - var len = keys.length; - while (true) { - if (_isArray(obj)) { - var index = parseInt(keys[t], 10); - t++; - if (t >= len) { - result = arrOps[patch.op].call(patch, obj, index, tree); - break; - } - obj = obj[index]; - } else { - var key = keys[t]; - if (key.indexOf('~') != -1) - key = key.replace(/~1/g, '/').replace(/~0/g, '~'); - t++; - if (t >= len) { - result = objOps[patch.op].call(patch, obj, key, tree); - break; - } - obj = obj[key]; - } - } - p++; - } - return result; - } - jsonpatch.apply = apply; -})(jsonpatch || (jsonpatch = {})); - -if (typeof exports !== "undefined") { - exports.apply = jsonpatch.apply; - exports.observe = jsonpatch.observe; - exports.unobserve = jsonpatch.unobserve; - exports.generate = jsonpatch.generate; -} - -Handsontable.PluginHookClass = (function () { - - var Hooks = function () { - return { - // Hooks - beforeInitWalkontable: [], - - beforeInit: [], - beforeRender: [], - beforeSetRangeEnd: [], - beforeChange: [], - beforeChangeRender: [], - beforeRemoveCol: [], - beforeRemoveRow: [], - beforeValidate: [], - beforeGetCellMeta: [], - beforeAutofill: [], - beforeKeyDown: [], - - afterInit : [], - afterLoadData : [], - afterUpdateSettings: [], - afterRender : [], - afterRenderer : [], - afterChange : [], - afterValidate: [], - afterGetCellMeta: [], - afterSetCellMeta: [], - afterGetColHeader: [], - afterGetRowHeader: [], - afterDestroy: [], - afterRemoveRow: [], - afterCreateRow: [], - afterRemoveCol: [], - afterCreateCol: [], - afterDeselect: [], - afterSelection: [], - afterSelectionByProp: [], - afterSelectionEnd: [], - afterSelectionEndByProp: [], - afterOnCellMouseDown: [], - afterOnCellMouseOver: [], - afterOnCellCornerMouseDown: [], - afterScrollVertically: [], - afterScrollHorizontally: [], - afterCellMetaReset:[], - afterDocumentKeyDown: [], - - // Modifiers - modifyColWidth: [], - modifyRowHeight: [], - modifyRow: [], - modifyCol: [] - } - }; - - var legacy = { - onBeforeChange: "beforeChange", - onChange: "afterChange", - onCreateRow: "afterCreateRow", - onCreateCol: "afterCreateCol", - onSelection: "afterSelection", - onCopyLimit: "afterCopyLimit", - onSelectionEnd: "afterSelectionEnd", - onSelectionByProp: "afterSelectionByProp", - onSelectionEndByProp: "afterSelectionEndByProp" - }; - - function PluginHookClass() { - - this.hooks = Hooks(); - this.globalBucket = {}; - this.legacy = legacy; - - } - - PluginHookClass.prototype.getBucket = function (instance) { - if(instance) { - if(!instance.pluginHookBucket) { - instance.pluginHookBucket = {}; - } - return instance.pluginHookBucket; - } - return this.globalBucket; - }; - - PluginHookClass.prototype.add = function (key, fn, instance) { - //if fn is array, run this for all the array items - if (Handsontable.helper.isArray(fn)) { - for (var i = 0, len = fn.length; i < len; i++) { - this.add(key, fn[i]); - } - } - else { - // provide support for old versions of HOT - if (key in legacy) { - key = legacy[key]; - } - - var bucket = this.getBucket(instance); - - if (typeof bucket[key] === "undefined") { - bucket[key] = []; - } - - fn.skip = false; - - if (bucket[key].indexOf(fn) == -1) { - bucket[key].push(fn); //only add a hook if it has not already be added (adding the same hook twice is now silently ignored) - } - } - return this; - }; - - PluginHookClass.prototype.once = function(key, fn, instance){ - - if(Handsontable.helper.isArray(fn)){ - - for(var i = 0, len = fn.length; i < len; i++){ - fn[i].runOnce = true; - this.add(key, fn[i], instance); - } - - } else { - fn.runOnce = true; - this.add(key, fn, instance); - - } - - }; - - PluginHookClass.prototype.remove = function (key, fn, instance) { - var status = false; - - // provide support for old versions of HOT - if (key in legacy) { - key = legacy[key]; - } - - var bucket = this.getBucket(instance); - - if (typeof bucket[key] !== 'undefined') { - - for (var i = 0, leni = bucket[key].length; i < leni; i++) { - - if (bucket[key][i] == fn) { - bucket[key][i].skip = true; - status = true; - break; - } - - } - - } - - return status; - }; - - PluginHookClass.prototype.run = function (instance, key, p1, p2, p3, p4, p5, p6) { - // provide support for old versions of HOT - if (key in legacy) { - key = legacy[key]; - } - - this._runBucket(this.globalBucket, instance, key, p1, p2, p3, p4, p5, p6); - this._runBucket(this.getBucket(instance), instance, key, p1, p2, p3, p4, p5, p6); - }; - - PluginHookClass.prototype._runBucket = function (bucket, instance, key, p1, p2, p3, p4, p5, p6) { - var handlers = bucket[key]; - if (handlers) { - for (var i = 0, leni = handlers.length; i < leni; i++) { - if (!handlers[i].skip) { - handlers[i].call(instance, p1, p2, p3, p4, p5, p6); - - if (handlers[i].runOnce) { - this.remove(key, handlers[i], bucket === this.globalBucket ? null : instance); - } - } - } - } - }; - - PluginHookClass.prototype.destroy = function (instance) { - var bucket = this.getBucket(instance); - for (var key in bucket) { - if (bucket.hasOwnProperty(key)) { - for (var i = 0, leni = bucket[key].length; i < leni; i++) { - this.remove(key, bucket[key], instance); - } - } - } - }; - - PluginHookClass.prototype.execute = function (instance, key, p1, p2, p3, p4, p5, p6) { - // provide support for old versions of HOT - if (key in legacy) { - key = legacy[key]; - } - - p1 = this._executeBucket(this.globalBucket, instance, key, p1, p2, p3, p4, p5, p6); - p1 = this._executeBucket(this.getBucket(instance), instance, key, p1, p2, p3, p4, p5, p6); - return p1; - }; - - PluginHookClass.prototype._executeBucket = function (bucket, instance, key, p1, p2, p3, p4, p5, p6) { - var res, - handlers = bucket[key]; - - //performance considerations - http://jsperf.com/call-vs-apply-for-a-plugin-architecture - if (handlers) { - for (var i = 0, leni = handlers.length; i < leni; i++) { - if (!handlers[i].skip) { - res = handlers[i].call(instance, p1, p2, p3, p4, p5, p6); - if (res !== void 0) { - p1 = res; - } - - if (handlers[i].runOnce) { - this.remove(key, handlers[i], bucket === this.globalBucket ? null : instance); - } - - if (res === false) { //if any handler returned false - return false; //event has been cancelled and further execution of handler queue is being aborted - } - } - } - } - - return p1; - }; - - /** - * Registers a hook name (adds it to the list of the known hook names). Used by plugins. It is not neccessary to call, - * register, but if you use it, your plugin hook will be used returned by getRegistered - * (which itself is used in the demo http://handsontable.com/demo/callbacks.html) - * @param key {String} - */ - PluginHookClass.prototype.register = function (key) { - if (!this.isRegistered(key)) { - this.hooks[key] = []; - } - }; - - /** - * Deregisters a hook name (removes it from the list of known hook names) - * @param key {String} - */ - PluginHookClass.prototype.deregister = function (key) { - delete this.hooks[key]; - }; - - /** - * Returns boolean information if a hook by such name has been registered - * @param key {String} - */ - PluginHookClass.prototype.isRegistered = function (key) { - return (typeof this.hooks[key] !== "undefined"); - }; - - /** - * Returns an array of registered hooks - * @returns {Array} - */ - PluginHookClass.prototype.getRegistered = function () { - return Object.keys(this.hooks); - }; - - return PluginHookClass; - -})(); - -Handsontable.hooks = new Handsontable.PluginHookClass(); -Handsontable.PluginHooks = Handsontable.hooks; //in future move this line to legacy.js - -(function (Handsontable) { - - function HandsontableAutoColumnSize() { - var plugin = this - , sampleCount = 5; //number of samples to take of each value length - - this.beforeInit = function () { - var instance = this; - instance.autoColumnWidths = []; - - if (instance.getSettings().autoColumnSize !== false) { - if (!instance.autoColumnSizeTmp) { - instance.autoColumnSizeTmp = { - table: null, - tableStyle: null, - theadTh: null, - tbody: null, - container: null, - containerStyle: null, - determineBeforeNextRender: true - }; - - instance.addHook('beforeRender', htAutoColumnSize.determineIfChanged); - instance.addHook('modifyColWidth', htAutoColumnSize.modifyColWidth); - instance.addHook('afterDestroy', htAutoColumnSize.afterDestroy); - - instance.determineColumnWidth = plugin.determineColumnWidth; - } - } else { - if (instance.autoColumnSizeTmp) { - instance.removeHook('beforeRender', htAutoColumnSize.determineIfChanged); - instance.removeHook('modifyColWidth', htAutoColumnSize.modifyColWidth); - instance.removeHook('afterDestroy', htAutoColumnSize.afterDestroy); - - delete instance.determineColumnWidth; - - plugin.afterDestroy.call(instance); - } - } - }; - - this.determineIfChanged = function (force) { - if (force) { - htAutoColumnSize.determineColumnsWidth.apply(this, arguments); - } - }; - - this.determineColumnWidth = function (col) { - var instance = this - , tmp = instance.autoColumnSizeTmp; - - if (!tmp.container) { - createTmpContainer.call(tmp, instance); - } - - tmp.container.className = instance.rootElement[0].className + ' htAutoColumnSize'; - tmp.table.className = instance.$table[0].className; - - var rows = instance.countRows(); - var samples = {}; - var maxLen = 0; - for (var r = 0; r < rows; r++) { - var value = Handsontable.helper.stringify(instance.getDataAtCell(r, col)); - var len = value.length; - if (len > maxLen) { - maxLen = len; - } - if (!samples[len]) { - samples[len] = { - needed: sampleCount, - strings: [] - }; - } - if (samples[len].needed) { - samples[len].strings.push({value: value, row: r}); - samples[len].needed--; - } - } - - var settings = instance.getSettings(); - if (settings.colHeaders) { - instance.view.appendColHeader(col, tmp.theadTh); //TH innerHTML - } - - Handsontable.Dom.empty(tmp.tbody); - - for (var i in samples) { - if (samples.hasOwnProperty(i)) { - for (var j = 0, jlen = samples[i].strings.length; j < jlen; j++) { - var row = samples[i].strings[j].row; - - var cellProperties = instance.getCellMeta(row, col); - cellProperties.col = col; - cellProperties.row = row; - - var renderer = instance.getCellRenderer(cellProperties); - - var tr = document.createElement('tr'); - var td = document.createElement('td'); - - renderer(instance, td, row, col, instance.colToProp(col), samples[i].strings[j].value, cellProperties); - r++; - tr.appendChild(td); - tmp.tbody.appendChild(tr); - } - } - } - - var parent = instance.rootElement[0].parentNode; - parent.appendChild(tmp.container); - var width = Handsontable.Dom.outerWidth(tmp.table); - parent.removeChild(tmp.container); - - return width; - }; - - this.determineColumnsWidth = function () { - var instance = this; - var settings = this.getSettings(); - if (settings.autoColumnSize || !settings.colWidths) { - var cols = this.countCols(); - for (var c = 0; c < cols; c++) { - if (!instance._getColWidthFromSettings(c)) { - this.autoColumnWidths[c] = plugin.determineColumnWidth.call(instance, c); - } - } - } - }; - - this.modifyColWidth = function (width, col) { - if (this.autoColumnWidths[col] && this.autoColumnWidths[col] > width) { - return this.autoColumnWidths[col]; - } - return width; - }; - - this.afterDestroy = function () { - var instance = this; - if (instance.autoColumnSizeTmp && instance.autoColumnSizeTmp.container && instance.autoColumnSizeTmp.container.parentNode) { - instance.autoColumnSizeTmp.container.parentNode.removeChild(instance.autoColumnSizeTmp.container); - } - instance.autoColumnSizeTmp = null; - }; - - function createTmpContainer(instance) { - var d = document - , tmp = this; - - tmp.table = d.createElement('table'); - tmp.theadTh = d.createElement('th'); - tmp.table.appendChild(d.createElement('thead')).appendChild(d.createElement('tr')).appendChild(tmp.theadTh); - - tmp.tableStyle = tmp.table.style; - tmp.tableStyle.tableLayout = 'auto'; - tmp.tableStyle.width = 'auto'; - - tmp.tbody = d.createElement('tbody'); - tmp.table.appendChild(tmp.tbody); - - tmp.container = d.createElement('div'); - tmp.container.className = instance.rootElement[0].className + ' hidden'; - tmp.containerStyle = tmp.container.style; - - tmp.container.appendChild(tmp.table); - } - } - - var htAutoColumnSize = new HandsontableAutoColumnSize(); - - Handsontable.hooks.add('beforeInit', htAutoColumnSize.beforeInit); - Handsontable.hooks.add('afterUpdateSettings', htAutoColumnSize.beforeInit); - -})(Handsontable); - -/** - * This plugin sorts the view by a column (but does not sort the data source!) - * @constructor - */ -function HandsontableColumnSorting() { - var plugin = this; - - this.init = function (source) { - var instance = this; - var sortingSettings = instance.getSettings().columnSorting; - var sortingColumn, sortingOrder; - - instance.sortingEnabled = !!(sortingSettings); - - if (instance.sortingEnabled) { - instance.sortIndex = []; - - var loadedSortingState = loadSortingState.call(instance); - - if (typeof loadedSortingState != 'undefined') { - sortingColumn = loadedSortingState.sortColumn; - sortingOrder = loadedSortingState.sortOrder; - } else { - sortingColumn = sortingSettings.column; - sortingOrder = sortingSettings.sortOrder; - } - plugin.sortByColumn.call(instance, sortingColumn, sortingOrder); - - instance.sort = function(){ - var args = Array.prototype.slice.call(arguments); - - return plugin.sortByColumn.apply(instance, args) - }; - - if (typeof instance.getSettings().observeChanges == 'undefined'){ - enableObserveChangesPlugin.call(instance); - } - - if (source == 'afterInit') { - bindColumnSortingAfterClick.call(instance); - - instance.addHook('afterCreateRow', plugin.afterCreateRow); - instance.addHook('afterRemoveRow', plugin.afterRemoveRow); - instance.addHook('afterLoadData', plugin.init); - } - } else { - delete instance.sort; - - instance.removeHook('afterCreateRow', plugin.afterCreateRow); - instance.removeHook('afterRemoveRow', plugin.afterRemoveRow); - instance.removeHook('afterLoadData', plugin.init); - } - }; - - this.setSortingColumn = function (col, order) { - var instance = this; - - if (typeof col == 'undefined') { - delete instance.sortColumn; - delete instance.sortOrder; - - return; - } else if (instance.sortColumn === col && typeof order == 'undefined') { - instance.sortOrder = !instance.sortOrder; - } else { - instance.sortOrder = typeof order != 'undefined' ? order : true; - } - - instance.sortColumn = col; - - }; - - this.sortByColumn = function (col, order) { - var instance = this; - - plugin.setSortingColumn.call(instance, col, order); - - if(typeof instance.sortColumn == 'undefined'){ - return; - } - - Handsontable.hooks.run(instance, 'beforeColumnSort', instance.sortColumn, instance.sortOrder); - - plugin.sort.call(instance); - instance.render(); - - saveSortingState.call(instance); - - Handsontable.hooks.run(instance, 'afterColumnSort', instance.sortColumn, instance.sortOrder); - }; - - var saveSortingState = function () { - var instance = this; - - var sortingState = {}; - - if (typeof instance.sortColumn != 'undefined') { - sortingState.sortColumn = instance.sortColumn; - } - - if (typeof instance.sortOrder != 'undefined') { - sortingState.sortOrder = instance.sortOrder; - } - - if (sortingState.hasOwnProperty('sortColumn') || sortingState.hasOwnProperty('sortOrder')) { - Handsontable.hooks.run(instance, 'persistentStateSave', 'columnSorting', sortingState); - } - - }; - - var loadSortingState = function () { - var instance = this; - var storedState = {}; - Handsontable.hooks.run(instance, 'persistentStateLoad', 'columnSorting', storedState); - - return storedState.value; - }; - - var bindColumnSortingAfterClick = function () { - var instance = this; - - instance.rootElement.on('click.handsontable', '.columnSorting', function (e) { - if (Handsontable.Dom.hasClass(e.target, 'columnSorting')) { - var col = getColumn(e.target); - plugin.sortByColumn.call(instance, col); - } - }); - - function countRowHeaders() { - var THs = instance.view.TBODY.querySelector('tr').querySelectorAll('th'); - return THs.length; - } - - function getColumn(target) { - var TH = Handsontable.Dom.closest(target, 'TH'); - return Handsontable.Dom.index(TH) - countRowHeaders(); - } - }; - - function enableObserveChangesPlugin () { - var instance = this; - instance._registerTimeout(setTimeout(function(){ - instance.updateSettings({ - observeChanges: true - }); - }, 0)); - } - - function defaultSort(sortOrder) { - return function (a, b) { - if (a[1] === b[1]) { - return 0; - } - if (a[1] === null) { - return 1; - } - if (b[1] === null) { - return -1; - } - if (a[1] < b[1]) return sortOrder ? -1 : 1; - if (a[1] > b[1]) return sortOrder ? 1 : -1; - return 0; - } - } - - function dateSort(sortOrder) { - return function (a, b) { - if (a[1] === b[1]) { - return 0; - } - if (a[1] === null) { - return 1; - } - if (b[1] === null) { - return -1; - } - - var aDate = new Date(a[1]); - var bDate = new Date(b[1]); - - if (aDate < bDate) return sortOrder ? -1 : 1; - if (aDate > bDate) return sortOrder ? 1 : -1; - - return 0; - } - } - - this.sort = function () { - var instance = this; - - if (typeof instance.sortOrder == 'undefined') { - return; - } - - instance.sortingEnabled = false; //this is required by translateRow plugin hook - instance.sortIndex.length = 0; - - var colOffset = this.colOffset(); - for (var i = 0, ilen = this.countRows() - instance.getSettings()['minSpareRows']; i < ilen; i++) { - this.sortIndex.push([i, instance.getDataAtCell(i, this.sortColumn + colOffset)]); - } - - var colMeta = instance.getCellMeta(0, instance.sortColumn); - var sortFunction; - switch (colMeta.type) { - case 'date': - sortFunction = dateSort; - break; - default: - sortFunction = defaultSort; - } - - this.sortIndex.sort(sortFunction(instance.sortOrder)); - - //Append spareRows - for(var i = this.sortIndex.length; i < instance.countRows(); i++){ - this.sortIndex.push([i, instance.getDataAtCell(i, this.sortColumn + colOffset)]); - } - - instance.sortingEnabled = true; //this is required by translateRow plugin hook - }; - - this.translateRow = function (row) { - var instance = this; - - if (instance.sortingEnabled && instance.sortIndex && instance.sortIndex.length && instance.sortIndex[row]) { - return instance.sortIndex[row][0]; - } - - return row; - }; - - this.untranslateRow = function (row) { - var instance = this; - if (instance.sortingEnabled && instance.sortIndex && instance.sortIndex.length) { - for (var i = 0; i < instance.sortIndex.length; i++) { - if (instance.sortIndex[i][0] == row) { - return i; - } - } - } - }; - - this.getColHeader = function (col, TH) { - if (this.getSettings().columnSorting) { - Handsontable.Dom.addClass(TH.querySelector('.colHeader'), 'columnSorting'); - } - }; - - function isSorted(instance){ - return typeof instance.sortColumn != 'undefined'; - } - - this.afterCreateRow = function(index, amount){ - var instance = this; - - if(!isSorted(instance)){ - return; - } - - - for(var i = 0; i < instance.sortIndex.length; i++){ - if (instance.sortIndex[i][0] >= index){ - instance.sortIndex[i][0] += amount; - } - } - - for(var i=0; i < amount; i++){ - instance.sortIndex.splice(index+i, 0, [index+i, instance.getData()[index+i][instance.sortColumn + instance.colOffset()]]); - } - - - - saveSortingState.call(instance); - - }; - - this.afterRemoveRow = function(index, amount){ - var instance = this; - - if(!isSorted(instance)){ - return; - } - - var physicalRemovedIndex = plugin.translateRow.call(instance, index); - - instance.sortIndex.splice(index, amount); - - for(var i = 0; i < instance.sortIndex.length; i++){ - - if (instance.sortIndex[i][0] > physicalRemovedIndex){ - instance.sortIndex[i][0] -= amount; - } - } - - saveSortingState.call(instance); - - }; - - this.afterChangeSort = function (changes/*, source*/) { - var instance = this; - var sortColumnChanged = false; - var selection = {}; - if (!changes) { - return; - } - - for (var i = 0; i < changes.length; i++) { - if (changes[i][1] == instance.sortColumn) { - sortColumnChanged = true; - selection.row = plugin.translateRow.call(instance, changes[i][0]); - selection.col = changes[i][1]; - break; - } - } - - if (sortColumnChanged) { - instance._registerTimeout(setTimeout(function () { - plugin.sort.call(instance); - instance.render(); - instance.selectCell(plugin.untranslateRow.call(instance, selection.row), selection.col); - }, 0)); - } - }; -} -var htSortColumn = new HandsontableColumnSorting(); - -Handsontable.hooks.add('afterInit', function () { - htSortColumn.init.call(this, 'afterInit') -}); -Handsontable.hooks.add('afterUpdateSettings', function () { - htSortColumn.init.call(this, 'afterUpdateSettings') -}); -Handsontable.hooks.add('modifyRow', htSortColumn.translateRow); -Handsontable.hooks.add('afterGetColHeader', htSortColumn.getColHeader); - -Handsontable.hooks.register('beforeColumnSort'); -Handsontable.hooks.register('afterColumnSort'); - - -(function (Handsontable) { - 'use strict'; - - function prepareVerticalAlignClass (className, alignment) { - if (className.indexOf(alignment)!= -1){ - return className; - } - - className = className - .replace('htTop','') - .replace('htMiddle','') - .replace('htBottom','') - .replace(' ',''); - - className += " " + alignment; - return className; - } - - function prepareHorizontalAlignClass (className, alignment) { - if (className.indexOf(alignment)!= -1){ - return className; - } - - className = className - .replace('htLeft','') - .replace('htCenter','') - .replace('htRight','') - .replace('htJustify','') - .replace(' ',''); - - className += " " + alignment; - return className; - } - - function doAlign (row, col, type, alignment) { - var cellMeta = this.getCellMeta(row, col), - className = alignment; - - if (cellMeta.className) { - if(type === 'vertical') { - className = prepareVerticalAlignClass(cellMeta.className, alignment); - } else { - className = prepareHorizontalAlignClass(cellMeta.className, alignment); - } - } - - this.setCellMeta(row, col, 'className',className); - this.render(); - } - - function align (range, type, alignment) { - if (range.from.row == range.to.row && range.from.col == range.to.col){ - doAlign.call(this,range.from.row, range.from.col, type, alignment); - } else { - for(var row = range.from.row; row<= range.to.row; row++) { - for (var col = range.from.col; col <= range.to.col; col++) { - doAlign.call(this,row, col, type, alignment); - } - } - } - } - - function ContextMenu(instance, customOptions){ - this.instance = instance; - var contextMenu = this; - contextMenu.menus = []; - contextMenu.triggerRows = []; - - this.enabled = true; - - this.instance.addHook('afterDestroy', function () { - contextMenu.destroy(); - }); - - this.defaultOptions = { - items: { - 'row_above': { - name: 'Insert row above', - callback: function(key, selection){ - this.alter("insert_row", selection.start.row); - }, - disabled: function () { - var selected = this.getSelected(), - entireColumnSelection = [0,selected[1],this.view.wt.wtTable.getRowStrategy().cellCount-1,selected[1]], - columnSelected = entireColumnSelection.join(',') == selected.join(','); - - return selected[0] < 0 || this.countRows() >= this.getSettings().maxRows || columnSelected; - } - }, - 'row_below': { - name: 'Insert row below', - callback: function(key, selection){ - this.alter("insert_row", selection.end.row + 1); - }, - disabled: function () { - var selected = this.getSelected(), - entireColumnSelection = [0,selected[1],this.view.wt.wtTable.getRowStrategy().cellCount-1,selected[1]], - columnSelected = entireColumnSelection.join(',') == selected.join(','); - - return this.getSelected()[0] < 0 || this.countRows() >= this.getSettings().maxRows || columnSelected; - } - }, - "hsep1": ContextMenu.SEPARATOR, - 'col_left': { - name: 'Insert column on the left', - callback: function(key, selection){ - this.alter("insert_col", selection.start.col); - }, - disabled: function () { - var selected = this.getSelected(), - entireRowSelection = [selected[0],0, selected[0],this.view.wt.wtTable.getColumnStrategy().cellCount-1], - rowSelected = entireRowSelection.join(',') == selected.join(','); - - return this.getSelected()[1] < 0 || this.countCols() >= this.getSettings().maxCols || rowSelected; - } - }, - 'col_right': { - name: 'Insert column on the right', - callback: function(key, selection){ - this.alter("insert_col", selection.end.col + 1); - }, - disabled: function () { - var selected = this.getSelected(), - entireRowSelection = [selected[0],0, selected[0],this.view.wt.wtTable.getColumnStrategy().cellCount-1], - rowSelected = entireRowSelection.join(',') == selected.join(','); - - return selected[1] < 0 || this.countCols() >= this.getSettings().maxCols || rowSelected; - } - }, - "hsep2": ContextMenu.SEPARATOR, - 'remove_row': { - name: 'Remove row', - callback: function(key, selection){ - var amount = selection.end.row - selection.start.row + 1; - this.alter("remove_row", selection.start.row, amount); - }, - disabled: function () { - var selected = this.getSelected(), - entireColumnSelection = [0,selected[1],this.view.wt.wtTable.getRowStrategy().cellCount-1,selected[1]], - columnSelected = entireColumnSelection.join(',') == selected.join(','); - return (selected[0] < 0 || columnSelected); - } - }, - 'remove_col': { - name: 'Remove column', - callback: function(key, selection){ - var amount = selection.end.col - selection.start.col + 1; - this.alter("remove_col", selection.start.col, amount); - }, - disabled: function (){ - var selected = this.getSelected(), - entireRowSelection = [selected[0],0, selected[0],this.view.wt.wtTable.getColumnStrategy().cellCount-1], - rowSelected = entireRowSelection.join(',') == selected.join(','); - return (selected[1] < 0 || rowSelected); - } - }, - "hsep3": ContextMenu.SEPARATOR, - 'undo': { - name: 'Undo', - callback: function(){ - this.undo(); - }, - disabled: function () { - return this.undoRedo && !this.undoRedo.isUndoAvailable(); - } - }, - 'redo': { - name: 'Redo', - callback: function(){ - this.redo(); - }, - disabled: function () { - return this.undoRedo && !this.undoRedo.isRedoAvailable(); - } - }, - "hsep4": ContextMenu.SEPARATOR, - 'make_read_only': { - name: function() { - var label = "Read only"; - var atLeastOneReadOnly = contextMenu.checkSelectionReadOnlyConsistency(this); - if (atLeastOneReadOnly) { - label = contextMenu.markSelected(label); - } - return label; - }, - callback: function() { - var atLeastOneReadOnly = contextMenu.checkSelectionReadOnlyConsistency(this); - - var that = this; - this.getSelectedRange().forAll(function(r, c) { - that.getCellMeta(r, c).readOnly = atLeastOneReadOnly ? false : true; - }); - - this.render(); - } - }, - "hsep5": ContextMenu.SEPARATOR, - 'aligment':{ - name: 'Alignment', - submenu: { - items: { - left: { - name: function () { - var label = "Left"; - var hasClass = contextMenu.checkSelectionAlignment(this,'htLeft'); - - if (hasClass){ - label = contextMenu.markSelected(label); - } - return label; - }, - callback: function (){ - align.call(this, this.getSelectedRange(),'horizontal','htLeft'); - }, - disabled: false - }, - center: { - name: function () { - var label = "Center"; - var hasClass = contextMenu.checkSelectionAlignment(this,'htCenter'); - - if (hasClass){ - label = contextMenu.markSelected(label); - } - return label; - }, - callback: function () { - align.call(this, this.getSelectedRange(),'horizontal','htCenter'); - }, - disabled: false - }, - right: { - name: function () { - var label = "Right"; - var hasClass = contextMenu.checkSelectionAlignment(this,'htRight'); - - if (hasClass){ - label = contextMenu.markSelected(label); - } - return label; - }, - callback: function () { - align.call(this, this.getSelectedRange(),'horizontal','htRight'); - }, - disabled: false - }, - justify: { - name: function () { - var label = "Justify"; - var hasClass = contextMenu.checkSelectionAlignment(this,'htJustify'); - - if (hasClass){ - label = contextMenu.markSelected(label); - } - return label; - }, - callback: function () { - align.call(this, this.getSelectedRange(),'horizontal','htJustify'); - }, - disabled: false - }, - "hsep1": ContextMenu.SEPARATOR, - top: { - name: function () { - var label = "Top"; - var hasClass = contextMenu.checkSelectionAlignment(this,'htTop'); - - if (hasClass){ - label = contextMenu.markSelected(label); - } - return label; - }, - callback: function() { - align.call(this, this.getSelectedRange(),'vertical','htTop'); - }, - disabled: false - }, - middle:{ - name: function () { - var label = "Middle"; - var hasClass = contextMenu.checkSelectionAlignment(this,'htMiddle'); - - if (hasClass){ - label = contextMenu.markSelected(label); - } - return label; - }, - callback: function() { - align.call(this, this.getSelectedRange(),'vertical','htMiddle'); - }, - disabled: false - }, - bottom: { - name: function () { - var label = "Bottom"; - var hasClass = contextMenu.checkSelectionAlignment(this,'htBottom'); - - if (hasClass){ - label = contextMenu.markSelected(label); - } - return label; - }, - callback: function() { - align.call(this, this.getSelectedRange(),'vertical','htBottom'); - }, - disabled: false - } - - } - } - } - } - }; - - contextMenu.options = {}; - - Handsontable.helper.extend(contextMenu.options, this.defaultOptions); - - this.updateOptions(customOptions, this.options); - - this.bindMouseEvents(); - - this.markSelected = function (label) { - return "" + label; - }; - - this.checkSelectionAlignment = function (hot, className){ - var hasAlignment = false; - - hot.getSelectedRange().forAll(function(r, c) { - var metaClassName = hot.getCellMeta(r, c).className; - if (metaClassName && metaClassName.indexOf(className)!= -1) { - hasAlignment = true; - return false; - } - }); - - return hasAlignment; - }; - - this.checkSelectionReadOnlyConsistency = function(hot) { - var atLeastOneReadOnly = false; - - hot.getSelectedRange().forAll(function(r, c) { - if(hot.getCellMeta(r, c).readOnly) { - atLeastOneReadOnly = true; - return false; //breaks forAll - } - }); - - return atLeastOneReadOnly; - }; - - Handsontable.hooks.run(instance, 'afterContextMenuDefaultOptions', this.defaultOptions); - - } - - ContextMenu.prototype.createMenu = function (menuName, row) { - if (menuName) { - menuName = menuName.replace(/ /g, '_'); // replace all spaces in name - menuName = 'htContextSubMenu_' + menuName; - } - - var menu; - if (menuName) { - menu = $('body > .htContextMenu.' + menuName)[0]; - } else { - menu = $('body > .htContextMenu')[0]; - } - - if(!menu){ - menu = document.createElement('DIV'); - Handsontable.Dom.addClass(menu, 'htContextMenu'); - if(menuName) { - Handsontable.Dom.addClass(menu, menuName); - } - document.getElementsByTagName('body')[0].appendChild(menu); - } - - if(this.menus.indexOf(menu) < 0){ - this.menus.push(menu); - row = row || 0; - this.triggerRows.push(row); - } - - return menu; - }; - - ContextMenu.prototype.bindMouseEvents = function (){ - function contextMenuOpenListener(event){ - this.closeAll(); - - event.preventDefault(); - event.stopPropagation(); - - var showRowHeaders = this.instance.getSettings().rowHeaders, - showColHeaders = this.instance.getSettings().colHeaders; - - if(!(showRowHeaders || showColHeaders)) { - if(event.target.nodeName != 'TD' && !(Handsontable.Dom.hasClass(event.target, 'current') && Handsontable.Dom.hasClass(event.target, 'wtBorder'))){ - return; - } - } - var menu = this.createMenu(); - var items = this.getItems(this.options); - - this.show(menu, items); - this.setMenuPosition(event, menu); - - $(document).on('mousedown.htContextMenu', Handsontable.helper.proxy(ContextMenu.prototype.closeAll, this)); - } - - this.instance.rootElement.on('contextmenu.htContextMenu', Handsontable.helper.proxy(contextMenuOpenListener, this)); - }; - - ContextMenu.prototype.bindTableEvents = function () { - var that = this; - - this._afterScrollCallback = function () { - // that.close(); - }; - - this.instance.addHook('afterScrollVertically', this._afterScrollCallback); - this.instance.addHook('afterScrollHorizontally', this._afterScrollCallback); - }; - - ContextMenu.prototype.unbindTableEvents = function () { - if(this._afterScrollCallback){ - this.instance.removeHook('afterScrollVertically', this._afterScrollCallback); - this.instance.removeHook('afterScrollHorizontally', this._afterScrollCallback); - this._afterScrollCallback = null; - } - }; - - ContextMenu.prototype.performAction = function (event, menu){ - var contextMenu = this; - var hot = $(menu).handsontable('getInstance'); - var selectedItemIndex = hot.getSelected()[0]; - var selectedItem = hot.getData()[selectedItemIndex]; - - if (selectedItem.disabled === true || (typeof selectedItem.disabled == 'function' && selectedItem.disabled.call(this.instance) === true)){ - return; - } - - if (!selectedItem.hasOwnProperty('submenu')) { - if(typeof selectedItem.callback != 'function'){ - return; - } - var selRange = this.instance.getSelectedRange(); - var normalizedSelection = ContextMenu.utils.normalizeSelection(selRange); - - selectedItem.callback.call(this.instance, selectedItem.key, normalizedSelection, event); - contextMenu.closeAll(); - this.instance.deselectCell(); - } - }; - - ContextMenu.prototype.unbindMouseEvents = function () { - this.instance.rootElement.off('contextmenu.htContextMenu'); - $(document).off('mousedown.htContextMenu'); - }; - - ContextMenu.prototype.show = function(menu, items){ - menu.style.display = 'block'; - - var that = this; - $(menu) - .off('mousedown.htContextMenu') - .on('mousedown.htContextMenu', function (event) { - that.performAction(event, menu) - }); - - $(menu).handsontable({ - data: ContextMenu.utils.convertItemsToArray(items), - colHeaders: false, - colWidths: [200], - readOnly: true, - copyPaste: false, - columns: [ - { - data: 'name', - renderer: Handsontable.helper.proxy(this.renderer, this) - } - ], - beforeKeyDown: function (event) { - that.onBeforeKeyDown(event, menu); - }, - afterOnCellMouseOver: function (event, coords, TD) { - that.onCellMouseOver(event, coords, TD, menu); - }, - - renderAllRows: true - }); - - this.bindTableEvents(); - - $(menu).handsontable('listen'); - - }; - - ContextMenu.prototype.close = function (menu) { - this.hide(menu); - $(document).off('mousedown.htContextMenu'); - this.unbindTableEvents(); - this.instance.listen(); - }; - - ContextMenu.prototype.closeAll = function () { - while(this.menus.length > 0) { - var menu = this.menus.pop(); - if (menu) { - this.close(menu); - } - - } - this.triggerRows = []; - }; - - ContextMenu.prototype.closeLastOpenedSubMenu = function (){ - var menu = this.menus.pop(); - if (menu) { - this.hide(menu); -// this.close(menu); - } - - }; - - ContextMenu.prototype.hide = function(menu){ - menu.style.display = 'none'; - $(menu).handsontable('destroy'); - }; - - ContextMenu.prototype.renderer = function(instance, TD, row, col, prop, value){ - var contextMenu = this; - var item = instance.getData()[row]; - var wrapper = document.createElement('DIV'); - - if(typeof value === 'function') { - value = value.call(this.instance); - } - - Handsontable.Dom.empty(TD); - TD.appendChild(wrapper); - - if(itemIsSeparator(item)){ - Handsontable.Dom.addClass(TD, 'htSeparator'); - } else { - Handsontable.Dom.fastInnerHTML(wrapper, value); - } - - if (itemIsDisabled(item)){ - Handsontable.Dom.addClass(TD, 'htDisabled'); - - $(wrapper).on('mouseenter', function () { - instance.deselectCell(); - }); - - } else { - if (isSubMenu(item)) { - Handsontable.Dom.addClass(TD, 'htSubmenu'); - - $(wrapper).on('mouseenter', function () { - instance.selectCell(row, col); - }); - - } else { - Handsontable.Dom.removeClass(TD, 'htSubmenu'); - Handsontable.Dom.removeClass(TD, 'htDisabled'); - - $(wrapper).on('mouseenter', function () { - instance.selectCell(row, col); - }); - } - } - - - function isSubMenu(item) { - return item.hasOwnProperty('submenu'); - } - - function itemIsSeparator(item){ - return new RegExp(ContextMenu.SEPARATOR, 'i').test(item.name); - } - - function itemIsDisabled(item){ - return item.disabled === true || (typeof item.disabled == 'function' && item.disabled.call(contextMenu.instance) === true); - } - - - - }; - - ContextMenu.prototype.onCellMouseOver = function (event, coords, TD, menu) { - - var hot = $(menu).handsontable('getInstance'); - var menusLength = this.menus.length; - - if (menusLength > 0) { - var lastMenu = this.menus[menusLength-1]; - if(lastMenu.id != menu.id) { - this.closeLastOpenedSubMenu(); - } - }else { - this.closeLastOpenedSubMenu(); - } - - if(TD.className.indexOf('htSubmenu')!=-1){ - var selectedItem = hot.getData()[coords.row]; - var items = this.getItems(selectedItem.submenu); - - var subMenu = this.createMenu(selectedItem.name, coords.row); - var tdCoords = TD.getBoundingClientRect(); - - this.show(subMenu, items); - this.setSubMenuPosition(tdCoords, subMenu); - - } - }; - - ContextMenu.prototype.onBeforeKeyDown = function (event, menu) { - var contextMenu = this; - var instance = $(menu).handsontable('getInstance'); - var selection = instance.getSelected(); - - switch(event.keyCode){ - - case Handsontable.helper.keyCode.ESCAPE: - contextMenu.closeAll(); - event.preventDefault(); - event.stopImmediatePropagation(); - break; - - case Handsontable.helper.keyCode.ENTER: - if(selection){ - contextMenu.performAction(event, menu); - } - break; - - case Handsontable.helper.keyCode.ARROW_DOWN: - - if(!selection){ - - selectFirstCell(instance, contextMenu); - - } else { - - selectNextCell(selection[0], selection[1], instance, contextMenu); - - } - - event.preventDefault(); - event.stopImmediatePropagation(); - - break; - - case Handsontable.helper.keyCode.ARROW_UP: - if(!selection){ - - selectLastCell(instance, contextMenu); - - } else { - - selectPrevCell(selection[0], selection[1], instance, contextMenu); - - } - - event.preventDefault(); - event.stopImmediatePropagation(); - - break; - case Handsontable.helper.keyCode.ARROW_RIGHT: - if(selection){ - var row = selection[0]; - var cell = instance.getCell(selection[0], 0); - - if(ContextMenu.utils.hasSubMenu(cell)) { - openSubMenu(instance, contextMenu, cell, row); - } - } - event.preventDefault(); - event.stopImmediatePropagation(); - - break; - - case Handsontable.helper.keyCode.ARROW_LEFT: - if (selection) { - - if (menu.className.indexOf('htContextSubMenu_')!= -1) { - contextMenu.closeLastOpenedSubMenu(); - var index = contextMenu.menus.length; - - if (index > 0){ - menu = contextMenu.menus[index - 1]; - var triggerRow = contextMenu.triggerRows.pop(); - instance = $(menu).handsontable('getInstance'); - instance.selectCell(triggerRow,0); - } - - } - - event.preventDefault(); - event.stopImmediatePropagation(); - } - break; - - - } - - function selectFirstCell(instance) { - - var firstCell = instance.getCell(0, 0); - - if(ContextMenu.utils.isSeparator(firstCell) || ContextMenu.utils.isDisabled(firstCell)){ - selectNextCell(0, 0, instance); - } else { - instance.selectCell(0, 0); - } - - } - - - function selectLastCell(instance) { - - var lastRow = instance.countRows() - 1; - var lastCell = instance.getCell(lastRow, 0); - - if(ContextMenu.utils.isSeparator(lastCell) || ContextMenu.utils.isDisabled(lastCell)){ - selectPrevCell(lastRow, 0, instance); - } else { - instance.selectCell(lastRow, 0); - } - - } - - function selectNextCell(row, col, instance){ - var nextRow = row + 1; - var nextCell = nextRow < instance.countRows() ? instance.getCell(nextRow, col) : null; - - if(!nextCell){ - return; - } - - if(ContextMenu.utils.isSeparator(nextCell) || ContextMenu.utils.isDisabled(nextCell)){ - selectNextCell(nextRow, col, instance); - } else { - instance.selectCell(nextRow, col); - } - } - - function selectPrevCell(row, col, instance) { - - var prevRow = row - 1; - var prevCell = prevRow >= 0 ? instance.getCell(prevRow, col) : null; - - if (!prevCell) { - return; - } - - if(ContextMenu.utils.isSeparator(prevCell) || ContextMenu.utils.isDisabled(prevCell)){ - selectPrevCell(prevRow, col, instance); - } else { - instance.selectCell(prevRow, col); - } - - } - - function openSubMenu (instance, contextMenu, cell, row) { - var selectedItem = instance.getData()[row]; - var items = contextMenu.getItems(selectedItem.submenu); - var subMenu = contextMenu.createMenu(selectedItem.name, row); - var coords = cell.getBoundingClientRect(); - - contextMenu.show(subMenu, items); - contextMenu.setSubMenuPosition(coords,subMenu); - var subMenuInstance = $(subMenu).handsontable('getInstance'); - subMenuInstance.selectCell(0,0); - } - - }; - - ContextMenu.prototype.getItems = function (options) { - var items = {}; - function Item(rawItem){ - if(typeof rawItem == 'string'){ - this.name = rawItem; - } else { - Handsontable.helper.extend(this, rawItem); - } - } - Item.prototype = options; - - for(var itemName in options.items){ - if(options.items.hasOwnProperty(itemName) && (!this.itemsFilter || this.itemsFilter.indexOf(itemName) != -1)){ - items[itemName] = new Item(options.items[itemName]); - } - } - return items; - - }; - - ContextMenu.prototype.updateOptions = function(newOptions, options){ - newOptions = newOptions || {}; - - if(newOptions.items){ - for(var itemName in newOptions.items){ - var item = {}; - - if(newOptions.items.hasOwnProperty(itemName)) { - if(this.defaultOptions.items.hasOwnProperty(itemName) - && Handsontable.helper.isObject(newOptions.items[itemName])){ - Handsontable.helper.extend(item, this.defaultOptions.items[itemName]); - Handsontable.helper.extend(item, newOptions.items[itemName]); - newOptions.items[itemName] = item; - } - } - - } - } - - Handsontable.helper.extend(options, newOptions); - }; - - ContextMenu.prototype.setSubMenuPosition = function (coords, menu) { - var scrollTop = Handsontable.Dom.getWindowScrollTop(); - var scrollLeft = Handsontable.Dom.getWindowScrollLeft(); - - var cursor = { - top: scrollTop + coords.top, - topRelative: coords.top , - left: coords.left, - leftRelative: coords.left - scrollLeft, - scrollTop: scrollTop, - scrollLeft: scrollLeft, - cellHeight: coords.height, - cellWidth: coords.width - }; - - if(this.menuFitsBelowCursor(cursor, menu)){ - this.positionMenuBelowCursor(cursor, menu, true); - } else { - if (this.menuFitsAboveCursor(cursor, menu)) { - this.positionMenuAboveCursor(cursor, menu, true); - } else { - this.positionMenuBelowCursor(cursor, menu, true); - } - } - - if(this.menuFitsOnRightOfCursor(cursor, menu)){ - this.positionMenuOnRightOfCursor(cursor, menu, true); - } else { - this.positionMenuOnLeftOfCursor(cursor, menu, true); - } - }; - - ContextMenu.prototype.setMenuPosition = function (event, menu) { - var cursorY = event.pageY; - var cursorX = event.pageX; - var scrollTop = Handsontable.Dom.getWindowScrollTop(); - var scrollLeft = Handsontable.Dom.getWindowScrollLeft(); - - var cursor = { - top: cursorY, - topRelative: cursorY - scrollTop, - left: cursorX, - leftRelative:cursorX - scrollLeft, - scrollTop: scrollTop, - scrollLeft: scrollLeft, - cellHeight: event.target.clientHeight, - cellWidth: event.target.clientWidth - }; - - if(this.menuFitsBelowCursor(cursor, menu)){ - this.positionMenuBelowCursor(cursor, menu); - } else { - if (this.menuFitsAboveCursor(cursor, menu)) { - this.positionMenuAboveCursor(cursor, menu); - } else { - this.positionMenuBelowCursor(cursor, menu); - } - } - - if(this.menuFitsOnRightOfCursor(cursor, menu)){ - this.positionMenuOnRightOfCursor(cursor, menu); - } else { - this.positionMenuOnLeftOfCursor(cursor, menu); - } - - }; - - ContextMenu.prototype.menuFitsAboveCursor = function (cursor, menu) { - return cursor.topRelative >= menu.offsetHeight; - }; - - ContextMenu.prototype.menuFitsBelowCursor = function (cursor, menu) { - return cursor.topRelative + menu.offsetHeight <= cursor.scrollTop + document.body.clientHeight; - }; - - ContextMenu.prototype.menuFitsOnRightOfCursor = function (cursor, menu) { - return cursor.leftRelative + menu.offsetWidth <= cursor.scrollLeft + document.body.clientWidth; - }; - - ContextMenu.prototype.positionMenuBelowCursor = function (cursor, menu) { - menu.style.top = cursor.top + 'px'; - }; - - ContextMenu.prototype.positionMenuAboveCursor = function (cursor, menu, subMenu) { - if (subMenu) { - menu.style.top = (cursor.top + cursor.cellHeight - menu.offsetHeight) + 'px'; - } else { - menu.style.top = (cursor.top - menu.offsetHeight) + 'px'; - } - }; - - ContextMenu.prototype.positionMenuOnRightOfCursor = function (cursor, menu, subMenu) { - if (subMenu) { - menu.style.left = cursor.left + cursor.cellWidth + 'px'; - } else { - menu.style.left = cursor.left + 'px'; - } - }; - - ContextMenu.prototype.positionMenuOnLeftOfCursor = function (cursor, menu, subMenu) { - if (subMenu) { - menu.style.left = (cursor.left - menu.offsetWidth) + 'px'; - } else { - menu.style.left = (cursor.left - menu.offsetWidth) + 'px'; - } - }; - - ContextMenu.utils = {}; - ContextMenu.utils.convertItemsToArray = function (items) { - var itemArray = []; - var item; - for(var itemName in items){ - if(items.hasOwnProperty(itemName)){ - if(typeof items[itemName] == 'string'){ - item = {name: items[itemName]}; - } else if (items[itemName].visible !== false) { - item = items[itemName]; - } else { - continue; - } - - item.key = itemName; - itemArray.push(item); - } - } - - return itemArray; - }; - - ContextMenu.utils.normalizeSelection = function(selRange){ - return { - start: selRange.getTopLeftCorner(), - end: selRange.getBottomRightCorner() - } - }; - - ContextMenu.utils.isSeparator = function (cell) { - return Handsontable.Dom.hasClass(cell, 'htSeparator'); - }; - - ContextMenu.utils.hasSubMenu = function (cell){ - return Handsontable.Dom.hasClass(cell, 'htSubmenu'); - }; - - ContextMenu.utils.isDisabled = function (cell) { - return Handsontable.Dom.hasClass(cell, 'htDisabled'); - }; - - ContextMenu.prototype.enable = function () { - if(!this.enabled){ - this.enabled = true; - this.bindMouseEvents(); - } - }; - - ContextMenu.prototype.disable = function () { - if(this.enabled){ - this.enabled = false; - this.closeAll(); - this.unbindMouseEvents(); - this.unbindTableEvents(); - } - }; - - ContextMenu.prototype.destroy = function () { - this.closeAll(); - while(this.menus.length > 0) { - var menu = this.menus.pop(); - this.triggerRows.pop(); - if (menu) { - this.close(menu); - if(!this.isMenuEnabledByOtherHotInstance()){ - this.removeMenu(menu); - } - } - } - - this.unbindMouseEvents(); - this.unbindTableEvents(); - - }; - - ContextMenu.prototype.isMenuEnabledByOtherHotInstance = function () { - var hotContainers = $('.handsontable'); - var menuEnabled = false; - - for(var i = 0, len = hotContainers.length; i < len; i++){ - var instance = $(hotContainers[i]).handsontable('getInstance'); - if(instance && instance.getSettings().contextMenu){ - menuEnabled = true; - break; - } - } - - return menuEnabled; - }; - - ContextMenu.prototype.removeMenu = function (menu) { - if(menu.parentNode){ - this.menu.parentNode.removeChild(menu); - } - }; - - ContextMenu.prototype.filterItems = function(itemsToLeave){ - this.itemsFilter = itemsToLeave; - }; - - ContextMenu.SEPARATOR = "---------"; - - function updateHeight() { - - if(this.rootElement[0].className.indexOf('htContextMenu')) { - return; - } - - var realSeparatorHeight = 0, - realEntrySize = 0, - dataSize = this.getSettings().data.length; - - for(var i = 0; i < dataSize; i++) { - if(this.getSettings().data[i].name == ContextMenu.SEPARATOR) { - realSeparatorHeight += 2; - } else { - realEntrySize += 26; - } - } - - this.view.wt.wtScrollbars.vertical.fixedContainer.style.height = realEntrySize + realSeparatorHeight + "px"; - } - - function init(){ - var instance = this; - var contextMenuSetting = instance.getSettings().contextMenu; - var customOptions = Handsontable.helper.isObject(contextMenuSetting) ? contextMenuSetting : {}; - - if(contextMenuSetting){ - if(!instance.contextMenu){ - instance.contextMenu = new ContextMenu(instance, customOptions); - } - - instance.contextMenu.enable(); - - if(Handsontable.helper.isArray(contextMenuSetting)){ - instance.contextMenu.filterItems(contextMenuSetting); - } - - } else if(instance.contextMenu){ - instance.contextMenu.destroy(); - delete instance.contextMenu; - } - - } - - Handsontable.hooks.add('afterInit', init); - Handsontable.hooks.add('afterUpdateSettings', init); - Handsontable.hooks.add('afterInit',updateHeight); - - if(Handsontable.PluginHooks.register) { //HOT 0.11+ - Handsontable.PluginHooks.register('afterContextMenuDefaultOptions'); - } - - - - Handsontable.ContextMenu = ContextMenu; - -})(Handsontable); - -function Comments(instance) { - - var doSaveComment = function (row, col, comment, instance) { - instance.setCellMeta(row, col, 'comment', comment); - instance.render(); - }, - saveComment = function (range, comment, instance) { - //LIKE IN EXCEL (TOP LEFT CELL) - doSaveComment(range.from.row, range.from.col, comment, instance); - }, - hideCommentTextArea = function () { - var commentBox = createCommentBox(); - commentBox.style.display = 'none'; - commentBox.value = ''; - }, - bindMouseEvent = function (range) { - - function commentsListener(event) { - $(document).off('mouseover.htCommment'); - if (!(event.target.className == 'htCommentTextArea' || event.target.innerHTML.indexOf('Comment') != -1)) { - var value = document.getElementsByClassName('htCommentTextArea')[0].value; - if (value.trim().length > 1) { - saveComment(range, value, instance); - } - unBindMouseEvent(); - hideCommentTextArea(); - } - } - - $(document).on('mousedown.htCommment', Handsontable.helper.proxy(commentsListener)); - }, - unBindMouseEvent = function () { - $(document).off('mousedown.htCommment'); - $(document).on('mouseover.htCommment', Handsontable.helper.proxy(commentsMouseOverListener)); - }, - placeCommentBox = function (range, commentBox) { - var TD = instance.view.wt.wtTable.getCell(range.from), - offset = Handsontable.Dom.offset(TD), - lastColWidth = instance.getColWidth(range.from.col); - - commentBox.style.position = 'absolute'; - commentBox.style.left = offset.left + lastColWidth + 'px'; - commentBox.style.top = offset.top + 'px'; - commentBox.style.zIndex = 2; - bindMouseEvent(range, commentBox); - }, - createCommentBox = function (value) { - var comments = document.getElementsByClassName('htComments')[0]; - - if (!comments) { - comments = document.createElement('DIV'); - - var textArea = document.createElement('TEXTAREA'); - Handsontable.Dom.addClass(textArea, 'htCommentTextArea'); - comments.appendChild(textArea); - - Handsontable.Dom.addClass(comments, 'htComments'); - document.getElementsByTagName('body')[0].appendChild(comments); - } - - value = value ||''; - - document.getElementsByClassName('htCommentTextArea')[0].value = value; - - //var tA = document.getElementsByClassName('htCommentTextArea')[0]; - //tA.focus(); - return comments; - }, - commentsMouseOverListener = function (event) { - if(event.target.className.indexOf('htCommentCell') != -1) { - unBindMouseEvent(); - var coords = instance.view.wt.wtTable.getCoords(event.target); - var range = { - from: new WalkontableCellCoords(coords.row, coords.col) - }; - - Handsontable.Comments.showComment(range); - } - else if(event.target.className !='htCommentTextArea'){ - hideCommentTextArea(); - } - }; - - return { - init: function () { - $(document).on('mouseover.htCommment', Handsontable.helper.proxy(commentsMouseOverListener)); - }, - showComment: function (range) { - var meta = instance.getCellMeta(range.from.row, range.from.col), - value = ''; - - if (meta.comment) { - value = meta.comment; - } - var commentBox = createCommentBox(value); - commentBox.style.display = 'block'; - placeCommentBox(range, commentBox); - }, - removeComment: function (row, col) { - instance.removeCellMeta(row, col, 'comment'); - instance.render(); - }, - checkSelectionCommentsConsistency : function () { - var hasComment = false; - // IN EXCEL THERE IS COMMENT ONLY FOR TOP LEFT CELL IN SELECTION - var cell = instance.getSelectedRange().from; - - if(instance.getCellMeta(cell.row,cell.col).comment) { - hasComment = true; - } - return hasComment; - } - - - }; -} - - -var init = function () { - var instance = this; - var commentsSetting = instance.getSettings().comments; - - if (commentsSetting) { - Handsontable.Comments = new Comments(instance); - Handsontable.Comments.init(); - } - }, - afterRenderer = function (TD, row, col, prop, value, cellProperties) { - if(cellProperties.comment) { - Handsontable.Dom.addClass(TD, cellProperties.commentedCellClassName); - } - }, - addCommentsActionsToContextMenu = function (defaultOptions) { - var instance = this; - if (!instance.getSettings().comments) { - return; - } - - defaultOptions.items.commentsCellsSeparator = Handsontable.ContextMenu.SEPARATOR; - - defaultOptions.items.commentsAddEdit = { - name: function () { - var hasComment = Handsontable.Comments.checkSelectionCommentsConsistency(); - return hasComment ? "Edit Comment" : "Add Comment"; - - }, - callback: function (key, selection, event) { - Handsontable.Comments.showComment(this.getSelectedRange()); - }, - disabled: function () { - return false; - } - }; - - defaultOptions.items.commentsRemove = { - name: function () { - return "Delete Comment" - }, - callback: function (key, selection, event) { - Handsontable.Comments.removeComment(selection.start.row, selection.start.col); - }, - disabled: function () { - var hasComment = Handsontable.Comments.checkSelectionCommentsConsistency(); - return !hasComment; - } - } - }; - -Handsontable.hooks.add('beforeInit', init); -Handsontable.hooks.add('afterContextMenuDefaultOptions', addCommentsActionsToContextMenu); -Handsontable.hooks.add('afterRenderer', afterRenderer); -//$(document).on('mouseover.htCommment', Handsontable.helper.proxy(commentsMouseOverListener)); - -/** - * HandsontableManualColumnMove - * - * Has 2 UI components: - * - handle - the draggable element that sets the desired position of the column - * - guide - the helper guide that shows the desired position as a vertical guide - * - * Warning! Whenever you make a change in this file, make an analogous change in manualRowMove.js - * @constructor - */ -(function (Handsontable) { -function HandsontableManualColumnMove() { - var startCol - , endCol - , startX - , startOffset - , currentCol - , instance - , currentTH - , handle = document.createElement('DIV') - , guide = document.createElement('DIV') - , $window = $(window); - - handle.className = 'manualColumnMover'; - guide.className = 'manualColumnMoverGuide'; - - var saveManualColumnPositions = function () { - var instance = this; - Handsontable.hooks.run(instance, 'persistentStateSave', 'manualColumnPositions', instance.manualColumnPositions); - }; - - var loadManualColumnPositions = function () { - var instance = this; - var storedState = {}; - Handsontable.hooks.run(instance, 'persistentStateLoad', 'manualColumnPositions', storedState); - return storedState.value; - }; - - function setupHandlePosition(TH) { - instance = this; - currentTH = TH; - - var col = this.view.wt.wtTable.getCoords(TH).col; //getCoords returns WalkontableCellCoords - if (col >= 0) { //if not row header - currentCol = col; - var box = currentTH.getBoundingClientRect(); - startOffset = box.left; - handle.style.top = box.top + 'px'; - handle.style.left = startOffset + 'px'; - instance.rootElement[0].appendChild(handle); - } - } - - function refreshHandlePosition(TH) { - var box = TH.getBoundingClientRect(); - handle.style.left = box.left + 'px'; - } - - function setupGuidePosition() { - var instance = this; - Handsontable.Dom.addClass(handle, 'active'); - Handsontable.Dom.addClass(guide, 'active'); - var box = currentTH.getBoundingClientRect(); - guide.style.width = box.width + 'px'; - guide.style.height = instance.view.maximumVisibleElementHeight(0) + 'px'; - guide.style.top = handle.style.top; - guide.style.left = startOffset + 'px'; - instance.rootElement[0].appendChild(guide); - } - - function refreshGuidePosition(diff) { - guide.style.left = startOffset + diff + 'px'; - } - - function hideHandleAndGuide() { - Handsontable.Dom.removeClass(handle, 'active'); - Handsontable.Dom.removeClass(guide, 'active'); - } - - var bindEvents = function () { - var instance = this; - var pressed; - - instance.rootElement.on('mouseenter.manualColumnMove.' + instance.guid, 'table thead tr > th', function (e) { - if (pressed) { - endCol = instance.view.wt.wtTable.getCoords(e.currentTarget).col; - refreshHandlePosition(e.currentTarget); - } - else { - setupHandlePosition.call(instance, e.currentTarget); - } - }); - - instance.rootElement.on('mousedown.manualColumnMove.' + instance.guid, '.manualColumnMover', function (e) { - startX = e.pageX; - setupGuidePosition.call(instance); - pressed = instance; - - startCol = currentCol; - endCol = currentCol; - }); - - $window.on('mousemove.manualColumnMove.' + instance.guid, function (e) { - if (pressed) { - refreshGuidePosition(e.pageX - startX); - } - }); - - $window.on('mouseup.manualColumnMove.' + instance.guid, function () { - if (pressed) { - hideHandleAndGuide(); - pressed = false; - - if (startCol < endCol) { - endCol--; - } - createPositionData(instance.manualColumnPositions, instance.countCols()); - instance.manualColumnPositions.splice(endCol, 0, instance.manualColumnPositions.splice(startCol, 1)[0]); - - instance.forceFullRender = true; - instance.view.render(); //updates all - - saveManualColumnPositions.call(instance); - - Handsontable.hooks.run(instance, 'afterColumnMove', startCol, endCol); - - setupHandlePosition.call(instance, currentTH); - } - }); - - instance.addHook('afterDestroy', unbindEvents); - }; - - var unbindEvents = function(){ - var instance = this; - instance.rootElement.off('mouseenter.manualColumnMove.' + instance.guid, 'table thead tr > th'); - instance.rootElement.off('mousedown.manualColumnMove.' + instance.guid, '.manualColumnMover'); - $window.off('mousemove.manualColumnMove.' + instance.guid); - $window.off('mouseup.manualColumnMove.' + instance.guid); - }; - - var createPositionData = function (positionArr, len) { - if (positionArr.length < len) { - for (var i = positionArr.length; i < len; i++) { - positionArr[i] = i; - } - } - }; - - this.beforeInit = function () { - this.manualColumnPositions = []; - }; - - this.init = function (source) { - var instance = this; - - var manualColMoveEnabled = !!(this.getSettings().manualColumnMove); - - if (manualColMoveEnabled) { - var initialManualColumnPositions = this.getSettings().manualColumnMove; - - var loadedManualColumnPositions = loadManualColumnPositions.call(instance); - - if (typeof loadedManualColumnPositions != 'undefined') { - this.manualColumnPositions = loadedManualColumnPositions; - } else if (initialManualColumnPositions instanceof Array) { - this.manualColumnPositions = initialManualColumnPositions; - } else { - this.manualColumnPositions = []; - } - - if (source == 'afterInit') { - bindEvents.call(this); - if (this.manualColumnPositions.length > 0) { - this.forceFullRender = true; - this.render(); - } - } - - } else { - unbindEvents.call(this); - this.manualColumnPositions = []; - } - }; - - this.modifyCol = function (col) { - //TODO test performance: http://jsperf.com/object-wrapper-vs-primitive/2 - if (this.getSettings().manualColumnMove) { - if (typeof this.manualColumnPositions[col] === 'undefined') { - createPositionData(this.manualColumnPositions, col + 1); - } - return this.manualColumnPositions[col]; - } - return col; - }; - -} -var htManualColumnMove = new HandsontableManualColumnMove(); - -Handsontable.hooks.add('beforeInit', htManualColumnMove.beforeInit); -Handsontable.hooks.add('afterInit', function () { - htManualColumnMove.init.call(this, 'afterInit') -}); - -Handsontable.hooks.add('afterUpdateSettings', function () { - htManualColumnMove.init.call(this, 'afterUpdateSettings') -}); -Handsontable.hooks.add('modifyCol', htManualColumnMove.modifyCol); - -Handsontable.hooks.register('afterColumnMove'); - -})(Handsontable); - - - -/** - * HandsontableManualColumnResize - * - * Has 2 UI components: - * - handle - the draggable element that sets the desired width of the column - * - guide - the helper guide that shows the desired width as a vertical guide - * - * Warning! Whenever you make a change in this file, make an analogous change in manualRowResize.js - * @constructor - */ -(function (Handsontable) { -function HandsontableManualColumnResize() { - var currentTH - , currentCol - , currentWidth - , instance - , newSize - , startX - , startWidth - , startOffset - , handle = document.createElement('DIV') - , guide = document.createElement('DIV') - , $window = $(window); - - handle.className = 'manualColumnResizer'; - guide.className = 'manualColumnResizerGuide'; - - var saveManualColumnWidths = function () { - var instance = this; - Handsontable.hooks.run(instance, 'persistentStateSave', 'manualColumnWidths', instance.manualColumnWidths); - }; - - var loadManualColumnWidths = function () { - var instance = this; - var storedState = {}; - Handsontable.hooks.run(instance, 'persistentStateLoad', 'manualColumnWidths', storedState); - return storedState.value; - }; - - function setupHandlePosition(TH) { - instance = this; - currentTH = TH; - - var col = this.view.wt.wtTable.getCoords(TH).col; //getCoords returns WalkontableCellCoords - if (col >= 0) { //if not row header - currentCol = col; - var box = currentTH.getBoundingClientRect(); - startOffset = box.left - 6; - startWidth = parseInt(box.width, 10); - handle.style.top = box.top + 'px'; - handle.style.left = startOffset + startWidth + 'px'; - instance.rootElement[0].appendChild(handle); - } - } - - function refreshHandlePosition() { - handle.style.left = startOffset + currentWidth + 'px'; - } - - function setupGuidePosition() { - var instance = this; - Handsontable.Dom.addClass(handle, 'active'); - Handsontable.Dom.addClass(guide, 'active'); - guide.style.top = handle.style.top; - guide.style.left = handle.style.left; - guide.style.height = instance.view.maximumVisibleElementHeight(0) + 'px'; - instance.rootElement[0].appendChild(guide); - } - - function refreshGuidePosition() { - guide.style.left = handle.style.left; - } - - function hideHandleAndGuide() { - Handsontable.Dom.removeClass(handle, 'active'); - Handsontable.Dom.removeClass(guide, 'active'); - } - - var bindEvents = function () { - var instance = this; - var pressed; - var dblclick = 0; - var autoresizeTimeout = null; - - instance.rootElement.on('mouseenter.manualColumnResize.' + instance.guid, 'table thead tr > th', function (e) { - if (!pressed) { - setupHandlePosition.call(instance, e.currentTarget); - } - }); - - instance.rootElement.on('mousedown.manualColumnResize.' + instance.guid, '.manualColumnResizer', function (e) { - setupGuidePosition.call(instance); - pressed = instance; - - if (autoresizeTimeout == null) { - autoresizeTimeout = setTimeout(function () { - if (dblclick >= 2) { - newSize = instance.determineColumnWidth.call(instance, currentCol); - setManualSize(currentCol, newSize); - instance.forceFullRender = true; - instance.view.render(); //updates all - Handsontable.hooks.run(instance, 'afterColumnResize', currentCol, newSize); - } - dblclick = 0; - autoresizeTimeout = null; - }, 500); - instance._registerTimeout(autoresizeTimeout); - } - dblclick++; - - startX = e.pageX; - newSize = startWidth; - }); - - $window.on('mousemove.manualColumnResize.' + instance.guid, function (e) { - if (pressed) { - currentWidth = startWidth + (e.pageX - startX); - newSize = setManualSize(currentCol, currentWidth); //save col width - refreshHandlePosition(); - refreshGuidePosition(); - } - }); - - $window.on('mouseup.manualColumnResize.' + instance.guid, function () { - if (pressed) { - hideHandleAndGuide(); - pressed = false; - - if(newSize != startWidth){ - instance.forceFullRender = true; - instance.view.render(); //updates all - - saveManualColumnWidths.call(instance); - - Handsontable.hooks.run(instance, 'afterColumnResize', currentCol, newSize); - } - - setupHandlePosition.call(instance, currentTH); - } - }); - - instance.addHook('afterDestroy', unbindEvents); - }; - - var unbindEvents = function(){ - var instance = this; - instance.rootElement.off('mouseenter.manualColumnResize.' + instance.guid, 'table thead tr > th'); - instance.rootElement.off('mousedown.manualColumnResize.' + instance.guid, '.manualColumnResizer'); - $window.off('mousemove.manualColumnResize.' + instance.guid); - $window.off('mouseup.manualColumnResize.' + instance.guid); - }; - - this.beforeInit = function () { - this.manualColumnWidths = []; - }; - - this.init = function (source) { - var instance = this; - var manualColumnWidthEnabled = !!(this.getSettings().manualColumnResize); - - if (manualColumnWidthEnabled) { - var initialColumnWidths = this.getSettings().manualColumnResize; - - var loadedManualColumnWidths = loadManualColumnWidths.call(instance); - - if (typeof loadedManualColumnWidths != 'undefined') { - this.manualColumnWidths = loadedManualColumnWidths; - } else if (initialColumnWidths instanceof Array) { - this.manualColumnWidths = initialColumnWidths; - } else { - this.manualColumnWidths = []; - } - - if (source == 'afterInit') { - bindEvents.call(this); - if (this.manualColumnWidths.length > 0) { - this.forceFullRender = true; - this.render(); - } - } - } - else { - unbindEvents.call(this); - this.manualColumnWidths = []; - } - }; - - - var setManualSize = function (col, width) { - width = Math.max(width, 20); - - /** - * We need to run col through modifyCol hook, in case the order of displayed columns is different than the order - * in data source. For instance, this order can be modified by manualColumnMove plugin. - */ - col = Handsontable.hooks.execute(instance, 'modifyCol', col); - - instance.manualColumnWidths[col] = width; - return width; - }; - - this.modifyColWidth = function (width, col) { - col = this.runHooksAndReturn('modifyCol', col); - if (this.getSettings().manualColumnResize && this.manualColumnWidths[col]) { - return this.manualColumnWidths[col]; - } - return width; - }; -} -var htManualColumnResize = new HandsontableManualColumnResize(); - -Handsontable.hooks.add('beforeInit', htManualColumnResize.beforeInit); -Handsontable.hooks.add('afterInit', function () { - htManualColumnResize.init.call(this, 'afterInit') -}); -Handsontable.hooks.add('afterUpdateSettings', function () { - htManualColumnResize.init.call(this, 'afterUpdateSettings') -}); -Handsontable.hooks.add('modifyColWidth', htManualColumnResize.modifyColWidth); - -Handsontable.hooks.register('afterColumnResize'); - -})(Handsontable); -/** - * HandsontableManualRowResize - * - * Has 2 UI components: - * - handle - the draggable element that sets the desired height of the row - * - guide - the helper guide that shows the desired height as a horizontal guide - * - * Warning! Whenever you make a change in this file, make an analogous change in manualRowResize.js - * @constructor - */ -(function (Handsontable) { - function HandsontableManualRowResize () { - - var currentTH - , currentRow - , currentHeight - , instance - , newSize - , startY - , startHeight - , startOffset - , handle = document.createElement('DIV') - , guide = document.createElement('DIV') - , $window = $(window); - - handle.className = 'manualRowResizer'; - guide.className = 'manualRowResizerGuide'; - - var saveManualRowHeights = function () { - var instance = this; - Handsontable.hooks.run(instance, 'persistentStateSave', 'manualRowHeights', instance.manualRowHeights); - }; - - var loadManualRowHeights = function () { - var instance = this - , storedState = {}; - Handsontable.hooks.run(instance, 'persistentStateLoad', 'manualRowHeights', storedState); - return storedState.value; - }; - - function setupHandlePosition(TH) { - instance = this; - currentTH = TH; - - var row = this.view.wt.wtTable.getCoords(TH).row; //getCoords returns WalkontableCellCoords - if (row >= 0) { //if not col header - currentRow = row; - var box = currentTH.getBoundingClientRect(); - startOffset = box.top - 6; - startHeight = parseInt(box.height, 10); - handle.style.left = box.left + 'px'; - handle.style.top = startOffset + startHeight + 'px'; - instance.rootElement[0].appendChild(handle); - } - } - - function refreshHandlePosition() { - handle.style.top = startOffset + currentHeight + 'px'; - } - - function setupGuidePosition() { - var instance = this; - Handsontable.Dom.addClass(handle, 'active'); - Handsontable.Dom.addClass(guide, 'active'); - guide.style.top = handle.style.top; - guide.style.left = handle.style.left; - guide.style.width = instance.view.maximumVisibleElementWidth(0) + 'px'; - instance.rootElement[0].appendChild(guide); - } - - function refreshGuidePosition() { - guide.style.top = handle.style.top; - } - - function hideHandleAndGuide() { - Handsontable.Dom.removeClass(handle, 'active'); - Handsontable.Dom.removeClass(guide, 'active'); - } - - var bindEvents = function () { - var instance = this; - var pressed; - var dblclick = 0; - var autoresizeTimeout = null; - - instance.rootElement.on('mouseenter.manualRowResize.' + instance.guid, 'table tbody tr > th', function (e) { - if (!pressed) { - setupHandlePosition.call(instance, e.currentTarget); - } - }); - - instance.rootElement.on('mousedown.manualRowResize.' + instance.guid, '.manualRowResizer', function (e) { - setupGuidePosition.call(instance); - pressed = instance; - - if (autoresizeTimeout == null) { - autoresizeTimeout = setTimeout(function () { - if (dblclick >= 2) { - setManualSize(currentRow, null); //double click sets auto row size - instance.forceFullRender = true; - instance.view.render(); //updates all - Handsontable.hooks.run(instance, 'afterRowResize', currentRow, newSize); - } - dblclick = 0; - autoresizeTimeout = null; - }, 500); - instance._registerTimeout(autoresizeTimeout); - } - dblclick++; - - startY = e.pageY; - newSize = startHeight; - }); - - $window.on('mousemove.manualRowResize.' + instance.guid, function (e) { - if (pressed) { - currentHeight = startHeight + (e.pageY - startY); - newSize = setManualSize(currentRow, currentHeight); - refreshHandlePosition(); - refreshGuidePosition(); - } - }); - - $window.on('mouseup.manualRowResize.' + instance.guid, function () { - if (pressed) { - hideHandleAndGuide(); - pressed = false; - - if(newSize != startHeight){ - instance.forceFullRender = true; - instance.view.render(); //updates all - - saveManualRowHeights.call(instance); - - Handsontable.hooks.run(instance, 'afterRowResize', currentRow, newSize); - } - - setupHandlePosition.call(instance, currentTH); - } - }); - - instance.addHook('afterDestroy', unbindEvents); - }; - - var unbindEvents = function(){ - var instance = this; - instance.rootElement.off('mouseenter.manualRowResize.' + instance.guid, 'table tbody tr > th'); - instance.rootElement.off('mousedown.manualRowResize.' + instance.guid, '.manualRowResizer'); - $window.off('mousemove.manualRowResize.' + instance.guid); - $window.off('mouseup.manualRowResize.' + instance.guid); - }; - - this.beforeInit = function () { - this.manualRowHeights = []; - }; - - this.init = function (source) { - - var instance = this; - var manualColumnHeightEnabled = !!(this.getSettings().manualRowResize); - - if (manualColumnHeightEnabled) { - - var initialRowHeights = this.getSettings().manualRowResize; - - var loadedManualRowHeights = loadManualRowHeights.call(instance); - - if (typeof loadedManualRowHeights != 'undefined') { - this.manualRowHeights = loadedManualRowHeights; - } else if (initialRowHeights instanceof Array) { - this.manualRowHeights = initialRowHeights; - } else { - this.manualRowHeights = []; - } - - if (source === 'afterInit') { - bindEvents.call(this); - if (this.manualRowHeights.length > 0) { - this.forceFullRender = true; - this.render(); - } - } - else { - this.forceFullRender = true; - this.render(); - - } - } - else { - unbindEvents.call(this); - this.manualRowHeights = []; - } - }; - - var setManualSize = function (row, height) { - row = Handsontable.hooks.execute(instance, 'modifyRow', row); - - instance.manualRowHeights[row] = height; - return height; - }; - - this.modifyRowHeight = function (height, row) { - if (this.getSettings().manualRowResize) { - row = this.runHooksAndReturn('modifyRow', row); - if (this.manualRowHeights[row] !== void 0) { - return this.manualRowHeights[row]; - } - } - return height; - }; - } - - var htManualRowResize = new HandsontableManualRowResize(); - - Handsontable.hooks.add('beforeInit', htManualRowResize.beforeInit); - Handsontable.hooks.add('afterInit', function () { - htManualRowResize.init.call(this, 'afterInit'); - }); - - Handsontable.hooks.add('afterUpdateSettings', function () { - htManualRowResize.init.call(this, 'afterUpdateSettings') - }); - - Handsontable.hooks.add('modifyRowHeight', htManualRowResize.modifyRowHeight); - - Handsontable.hooks.register('afterRowResize'); - -})(Handsontable); - -(function HandsontableObserveChanges() { - - Handsontable.hooks.add('afterLoadData', init); - Handsontable.hooks.add('afterUpdateSettings', init); - - Handsontable.hooks.register('afterChangesObserved'); - - function init() { - var instance = this; - var pluginEnabled = instance.getSettings().observeChanges; - - if (pluginEnabled) { - if(instance.observer) { - destroy.call(instance); //destroy observer for old data object - } - createObserver.call(instance); - bindEvents.call(instance); - - } else if (!pluginEnabled){ - destroy.call(instance); - } - } - - function createObserver(){ - var instance = this; - - instance.observeChangesActive = true; - - instance.pauseObservingChanges = function(){ - instance.observeChangesActive = false; - }; - - instance.resumeObservingChanges = function(){ - instance.observeChangesActive = true; - }; - - instance.observedData = instance.getData(); - instance.observer = jsonpatch.observe(instance.observedData, function (patches) { - if(instance.observeChangesActive){ - runHookForOperation.call(instance, patches); - instance.render(); - } - - instance.runHooks('afterChangesObserved'); - }); - } - - function runHookForOperation(rawPatches){ - var instance = this; - var patches = cleanPatches(rawPatches); - - for(var i = 0, len = patches.length; i < len; i++){ - var patch = patches[i]; - var parsedPath = parsePath(patch.path); - - - switch(patch.op){ - case 'add': - if(isNaN(parsedPath.col)){ - instance.runHooks('afterCreateRow', parsedPath.row); - } else { - instance.runHooks('afterCreateCol', parsedPath.col); - } - break; - - case 'remove': - if(isNaN(parsedPath.col)){ - instance.runHooks('afterRemoveRow', parsedPath.row, 1); - } else { - instance.runHooks('afterRemoveCol', parsedPath.col, 1); - } - break; - - case 'replace': - instance.runHooks('afterChange', [parsedPath.row, parsedPath.col, null, patch.value], 'external'); - break; - } - } - - function cleanPatches(rawPatches){ - var patches; - - patches = removeLengthRelatedPatches(rawPatches); - patches = removeMultipleAddOrRemoveColPatches(patches); - - return patches; - } - - /** - * Removing or adding column will produce one patch for each table row. - * This function leaves only one patch for each column add/remove operation - */ - function removeMultipleAddOrRemoveColPatches(rawPatches){ - var newOrRemovedColumns = []; - - return rawPatches.filter(function(patch){ - var parsedPath = parsePath(patch.path); - - if(['add', 'remove'].indexOf(patch.op) != -1 && !isNaN(parsedPath.col)){ - if(newOrRemovedColumns.indexOf(parsedPath.col) != -1){ - return false; - } else { - newOrRemovedColumns.push(parsedPath.col); - } - } - - return true; - }); - - } - - /** - * If observeChanges uses native Object.observe method, then it produces patches for length property. - * This function removes them. - */ - function removeLengthRelatedPatches(rawPatches){ - return rawPatches.filter(function(patch){ - return !/[/]length/ig.test(patch.path); - }) - } - - function parsePath(path){ - var match = path.match(/^\/(\d+)\/?(.*)?$/); - return { - row: parseInt(match[1], 10), - col: /^\d*$/.test(match[2]) ? parseInt(match[2], 10) : match[2] - } - } - } - - function destroy(){ - var instance = this; - - if (instance.observer){ - destroyObserver.call(instance); - unbindEvents.call(instance); - } - } - - function destroyObserver(){ - var instance = this; - - jsonpatch.unobserve(instance.observedData, instance.observer); - delete instance.observeChangesActive; - delete instance.pauseObservingChanges; - delete instance.resumeObservingChanges; - } - - function bindEvents(){ - var instance = this; - instance.addHook('afterDestroy', destroy); - - instance.addHook('afterCreateRow', afterTableAlter); - instance.addHook('afterRemoveRow', afterTableAlter); - - instance.addHook('afterCreateCol', afterTableAlter); - instance.addHook('afterRemoveCol', afterTableAlter); - - instance.addHook('afterChange', function(changes, source){ - if(source != 'loadData'){ - afterTableAlter.call(this); - } - }); - } - - function unbindEvents(){ - var instance = this; - instance.removeHook('afterDestroy', destroy); - - instance.removeHook('afterCreateRow', afterTableAlter); - instance.removeHook('afterRemoveRow', afterTableAlter); - - instance.removeHook('afterCreateCol', afterTableAlter); - instance.removeHook('afterRemoveCol', afterTableAlter); - - instance.removeHook('afterChange', afterTableAlter); - } - - function afterTableAlter(){ - var instance = this; - - instance.pauseObservingChanges(); - - instance.addHookOnce('afterChangesObserved', function(){ - instance.resumeObservingChanges(); - }); - - } -})(); - - -/* - * - * Plugin enables saving table state - * - * */ - - -function Storage(prefix) { - - var savedKeys; - - var saveSavedKeys = function () { - window.localStorage[prefix + '__' + 'persistentStateKeys'] = JSON.stringify(savedKeys); - }; - - var loadSavedKeys = function () { - var keysJSON = window.localStorage[prefix + '__' + 'persistentStateKeys']; - var keys = typeof keysJSON == 'string' ? JSON.parse(keysJSON) : void 0; - savedKeys = keys ? keys : []; - }; - - var clearSavedKeys = function () { - savedKeys = []; - saveSavedKeys(); - }; - - loadSavedKeys(); - - this.saveValue = function (key, value) { - window.localStorage[prefix + '_' + key] = JSON.stringify(value); - if (savedKeys.indexOf(key) == -1) { - savedKeys.push(key); - saveSavedKeys(); - } - - }; - - this.loadValue = function (key, defaultValue) { - - key = typeof key != 'undefined' ? key : defaultValue; - - var value = window.localStorage[prefix + '_' + key]; - - return typeof value == "undefined" ? void 0 : JSON.parse(value); - - }; - - this.reset = function (key) { - window.localStorage.removeItem(prefix + '_' + key); - }; - - this.resetAll = function () { - for (var index = 0; index < savedKeys.length; index++) { - window.localStorage.removeItem(prefix + '_' + savedKeys[index]); - } - - clearSavedKeys(); - }; - -} - - -(function (StorageClass) { - function HandsontablePersistentState() { - var plugin = this; - - - this.init = function () { - var instance = this, - pluginSettings = instance.getSettings()['persistentState']; - - plugin.enabled = !!(pluginSettings); - - if (!plugin.enabled) { - removeHooks.call(instance); - return; - } - - if (!instance.storage) { - instance.storage = new StorageClass(instance.rootElement[0].id); - } - - instance.resetState = plugin.resetValue; - - addHooks.call(instance); - - }; - - this.saveValue = function (key, value) { - var instance = this; - - instance.storage.saveValue(key, value); - }; - - this.loadValue = function (key, saveTo) { - var instance = this; - - saveTo.value = instance.storage.loadValue(key); - }; - - this.resetValue = function (key) { - var instance = this; - - if (typeof key != 'undefined') { - instance.storage.reset(key); - } else { - instance.storage.resetAll(); - } - - }; - - var hooks = { - 'persistentStateSave': plugin.saveValue, - 'persistentStateLoad': plugin.loadValue, - 'persistentStateReset': plugin.resetValue - }; - - for (var hookName in hooks) { - if (hooks.hasOwnProperty(hookName)) { - Handsontable.hooks.register(hookName); - } - } - - function addHooks() { - var instance = this; - - for (var hookName in hooks) { - if (hooks.hasOwnProperty(hookName)) { - instance.addHook(hookName, hooks[hookName]); - } - } - } - - function removeHooks() { - var instance = this; - - for (var hookName in hooks) { - if (hooks.hasOwnProperty(hookName)) { - instance.removeHook(hookName, hooks[hookName]); - } - } - } - } - - var htPersistentState = new HandsontablePersistentState(); - Handsontable.hooks.add('beforeInit', htPersistentState.init); - Handsontable.hooks.add('afterUpdateSettings', htPersistentState.init); -})(Storage); - -/** - * Handsontable UndoRedo class - */ -(function(Handsontable){ - Handsontable.UndoRedo = function (instance) { - var plugin = this; - this.instance = instance; - this.doneActions = []; - this.undoneActions = []; - this.ignoreNewActions = false; - instance.addHook("afterChange", function (changes, origin) { - if(changes){ - var action = new Handsontable.UndoRedo.ChangeAction(changes); - plugin.done(action); - } - }); - - instance.addHook("afterCreateRow", function (index, amount, createdAutomatically) { - - if (createdAutomatically) { - return; - } - - var action = new Handsontable.UndoRedo.CreateRowAction(index, amount); - plugin.done(action); - }); - - instance.addHook("beforeRemoveRow", function (index, amount) { - var originalData = plugin.instance.getData(); - index = ( originalData.length + index ) % originalData.length; - var removedData = originalData.slice(index, index + amount); - var action = new Handsontable.UndoRedo.RemoveRowAction(index, removedData); - plugin.done(action); - }); - - instance.addHook("afterCreateCol", function (index, amount, createdAutomatically) { - - if (createdAutomatically) { - return; - } - - var action = new Handsontable.UndoRedo.CreateColumnAction(index, amount); - plugin.done(action); - }); - - instance.addHook("beforeRemoveCol", function (index, amount) { - var originalData = plugin.instance.getData(); - index = ( plugin.instance.countCols() + index ) % plugin.instance.countCols(); - var removedData = []; - - for (var i = 0, len = originalData.length; i < len; i++) { - removedData[i] = originalData[i].slice(index, index + amount); - } - - var headers; - if(Handsontable.helper.isArray(instance.getSettings().colHeaders)){ - headers = instance.getSettings().colHeaders.slice(index, index + removedData.length); - } - - var action = new Handsontable.UndoRedo.RemoveColumnAction(index, removedData, headers); - plugin.done(action); - }); - }; - - Handsontable.UndoRedo.prototype.done = function (action) { - if (!this.ignoreNewActions) { - this.doneActions.push(action); - this.undoneActions.length = 0; - } - }; - - /** - * Undo operation from current revision - */ - Handsontable.UndoRedo.prototype.undo = function () { - if (this.isUndoAvailable()) { - var action = this.doneActions.pop(); - - this.ignoreNewActions = true; - var that = this; - action.undo(this.instance, function () { - that.ignoreNewActions = false; - that.undoneActions.push(action); - }); - - - - } - }; - - /** - * Redo operation from current revision - */ - Handsontable.UndoRedo.prototype.redo = function () { - if (this.isRedoAvailable()) { - var action = this.undoneActions.pop(); - - this.ignoreNewActions = true; - var that = this; - action.redo(this.instance, function () { - that.ignoreNewActions = false; - that.doneActions.push(action); - }); - - - - } - }; - - /** - * Returns true if undo point is available - * @return {Boolean} - */ - Handsontable.UndoRedo.prototype.isUndoAvailable = function () { - return this.doneActions.length > 0; - }; - - /** - * Returns true if redo point is available - * @return {Boolean} - */ - Handsontable.UndoRedo.prototype.isRedoAvailable = function () { - return this.undoneActions.length > 0; - }; - - /** - * Clears undo history - */ - Handsontable.UndoRedo.prototype.clear = function () { - this.doneActions.length = 0; - this.undoneActions.length = 0; - }; - - Handsontable.UndoRedo.Action = function () { - }; - Handsontable.UndoRedo.Action.prototype.undo = function () { - }; - Handsontable.UndoRedo.Action.prototype.redo = function () { - }; - - Handsontable.UndoRedo.ChangeAction = function (changes) { - this.changes = changes; - }; - Handsontable.helper.inherit(Handsontable.UndoRedo.ChangeAction, Handsontable.UndoRedo.Action); - Handsontable.UndoRedo.ChangeAction.prototype.undo = function (instance, undoneCallback) { - var data = $.extend(true, [], this.changes), - emptyRowsAtTheEnd = instance.countEmptyRows(true), - emptyColsAtTheEnd = instance.countEmptyCols(true); - - for (var i = 0, len = data.length; i < len; i++) { - data[i].splice(3, 1); - } - - instance.addHookOnce('afterChange', undoneCallback); - - instance.setDataAtRowProp(data, null, null, 'undo'); - - for (var i = 0, len = data.length; i < len; i++) { - if(instance.getSettings().minSpareRows && - data[i][0] + 1 + instance.getSettings().minSpareRows === instance.countRows() - && emptyRowsAtTheEnd == instance.getSettings().minSpareRows) { - instance.alter('remove_row', parseInt(data[i][0]+1,10), instance.getSettings().minSpareRows); - - instance.undoRedo.doneActions.pop(); - - } - - if (instance.getSettings().minSpareCols && - data[i][1] + 1 + instance.getSettings().minSpareCols === instance.countCols() - && emptyColsAtTheEnd == instance.getSettings().minSpareCols) { - instance.alter('remove_col', parseInt(data[i][1]+1,10), instance.getSettings().minSpareCols); - - instance.undoRedo.doneActions.pop(); - } - } - - }; - Handsontable.UndoRedo.ChangeAction.prototype.redo = function (instance, onFinishCallback) { - var data = $.extend(true, [], this.changes); - for (var i = 0, len = data.length; i < len; i++) { - data[i].splice(2, 1); - } - - instance.addHookOnce('afterChange', onFinishCallback); - - instance.setDataAtRowProp(data, null, null, 'redo'); - - }; - - Handsontable.UndoRedo.CreateRowAction = function (index, amount) { - this.index = index; - this.amount = amount; - }; - Handsontable.helper.inherit(Handsontable.UndoRedo.CreateRowAction, Handsontable.UndoRedo.Action); - Handsontable.UndoRedo.CreateRowAction.prototype.undo = function (instance, undoneCallback) { - instance.addHookOnce('afterRemoveRow', undoneCallback); - instance.alter('remove_row', this.index, this.amount); - }; - Handsontable.UndoRedo.CreateRowAction.prototype.redo = function (instance, redoneCallback) { - instance.addHookOnce('afterCreateRow', redoneCallback); - instance.alter('insert_row', this.index + 1, this.amount); - }; - - Handsontable.UndoRedo.RemoveRowAction = function (index, data) { - this.index = index; - this.data = data; - }; - Handsontable.helper.inherit(Handsontable.UndoRedo.RemoveRowAction, Handsontable.UndoRedo.Action); - Handsontable.UndoRedo.RemoveRowAction.prototype.undo = function (instance, undoneCallback) { - var spliceArgs = [this.index, 0]; - Array.prototype.push.apply(spliceArgs, this.data); - - Array.prototype.splice.apply(instance.getData(), spliceArgs); - - instance.addHookOnce('afterRender', undoneCallback); - instance.render(); - }; - Handsontable.UndoRedo.RemoveRowAction.prototype.redo = function (instance, redoneCallback) { - instance.addHookOnce('afterRemoveRow', redoneCallback); - instance.alter('remove_row', this.index, this.data.length); - }; - - Handsontable.UndoRedo.CreateColumnAction = function (index, amount) { - this.index = index; - this.amount = amount; - }; - Handsontable.helper.inherit(Handsontable.UndoRedo.CreateColumnAction, Handsontable.UndoRedo.Action); - Handsontable.UndoRedo.CreateColumnAction.prototype.undo = function (instance, undoneCallback) { - instance.addHookOnce('afterRemoveCol', undoneCallback); - instance.alter('remove_col', this.index, this.amount); - }; - Handsontable.UndoRedo.CreateColumnAction.prototype.redo = function (instance, redoneCallback) { - instance.addHookOnce('afterCreateCol', redoneCallback); - instance.alter('insert_col', this.index + 1, this.amount); - }; - - Handsontable.UndoRedo.RemoveColumnAction = function (index, data, headers) { - this.index = index; - this.data = data; - this.amount = this.data[0].length; - this.headers = headers; - }; - Handsontable.helper.inherit(Handsontable.UndoRedo.RemoveColumnAction, Handsontable.UndoRedo.Action); - Handsontable.UndoRedo.RemoveColumnAction.prototype.undo = function (instance, undoneCallback) { - var row, spliceArgs; - for (var i = 0, len = instance.getData().length; i < len; i++) { - row = instance.getSourceDataAtRow(i); - - spliceArgs = [this.index, 0]; - Array.prototype.push.apply(spliceArgs, this.data[i]); - - Array.prototype.splice.apply(row, spliceArgs); - - } - - if(typeof this.headers != 'undefined'){ - spliceArgs = [this.index, 0]; - Array.prototype.push.apply(spliceArgs, this.headers); - Array.prototype.splice.apply(instance.getSettings().colHeaders, spliceArgs); - } - - instance.addHookOnce('afterRender', undoneCallback); - instance.render(); - }; - Handsontable.UndoRedo.RemoveColumnAction.prototype.redo = function (instance, redoneCallback) { - instance.addHookOnce('afterRemoveCol', redoneCallback); - instance.alter('remove_col', this.index, this.amount); - }; -})(Handsontable); - -(function(Handsontable){ - - function init(){ - var instance = this; - var pluginEnabled = typeof instance.getSettings().undo == 'undefined' || instance.getSettings().undo; - - if(pluginEnabled){ - if(!instance.undoRedo){ - instance.undoRedo = new Handsontable.UndoRedo(instance); - - exposeUndoRedoMethods(instance); - - instance.addHook('beforeKeyDown', onBeforeKeyDown); - instance.addHook('afterChange', onAfterChange); - } - } else { - if(instance.undoRedo){ - delete instance.undoRedo; - - removeExposedUndoRedoMethods(instance); - - instance.removeHook('beforeKeyDown', onBeforeKeyDown); - instance.removeHook('afterChange', onAfterChange); - } - } - } - - function onBeforeKeyDown(event){ - var instance = this; - - var ctrlDown = (event.ctrlKey || event.metaKey) && !event.altKey; - - if(ctrlDown){ - if (event.keyCode === 89 || (event.shiftKey && event.keyCode === 90)) { //CTRL + Y or CTRL + SHIFT + Z - instance.undoRedo.redo(); - event.stopImmediatePropagation(); - } - else if (event.keyCode === 90) { //CTRL + Z - instance.undoRedo.undo(); - event.stopImmediatePropagation(); - } - } - } - - function onAfterChange(changes, source){ - var instance = this; - if (source == 'loadData'){ - return instance.undoRedo.clear(); - } - } - - function exposeUndoRedoMethods(instance){ - instance.undo = function(){ - return instance.undoRedo.undo(); - }; - - instance.redo = function(){ - return instance.undoRedo.redo(); - }; - - instance.isUndoAvailable = function(){ - return instance.undoRedo.isUndoAvailable(); - }; - - instance.isRedoAvailable = function(){ - return instance.undoRedo.isRedoAvailable(); - }; - - instance.clearUndo = function(){ - return instance.undoRedo.clear(); - }; - } - - function removeExposedUndoRedoMethods(instance){ - delete instance.undo; - delete instance.redo; - delete instance.isUndoAvailable; - delete instance.isRedoAvailable; - delete instance.clearUndo; - } - - Handsontable.hooks.add('afterInit', init); - Handsontable.hooks.add('afterUpdateSettings', init); - -})(Handsontable); - -/** - * Plugin used to scroll Handsontable by selecting a cell and dragging outside of visible viewport - * @constructor - */ -function DragToScroll() { - this.boundaries = null; - this.callback = null; -} - -/** - * @param boundaries {Object} compatible with getBoundingClientRect - */ -DragToScroll.prototype.setBoundaries = function (boundaries) { - this.boundaries = boundaries; -}; - -/** - * @param callback {Function} - */ -DragToScroll.prototype.setCallback = function (callback) { - this.callback = callback; -}; - -/** - * Check if mouse position (x, y) is outside of the viewport - * @param x - * @param y - */ -DragToScroll.prototype.check = function (x, y) { - var diffX = 0; - var diffY = 0; - - if (y < this.boundaries.top) { - //y is less than top - diffY = y - this.boundaries.top; - } - else if (y > this.boundaries.bottom) { - //y is more than bottom - diffY = y - this.boundaries.bottom; - } - - if (x < this.boundaries.left) { - //x is less than left - diffX = x - this.boundaries.left; - } - else if (x > this.boundaries.right) { - //x is more than right - diffX = x - this.boundaries.right; - } - - this.callback(diffX, diffY); -}; - -var dragToScroll; -var instance; - -if (typeof Handsontable !== 'undefined') { - var setupListening = function (instance) { - instance.dragToScrollListening = false; - var scrollHandler = instance.view.wt.wtScrollbars.vertical.scrollHandler; //native scroll - dragToScroll = new DragToScroll(); - if (scrollHandler === window) { - //not much we can do currently - return; - } - else if (scrollHandler) { - dragToScroll.setBoundaries(scrollHandler.getBoundingClientRect()); - } - else { - dragToScroll.setBoundaries(instance.$table[0].getBoundingClientRect()); - } - - dragToScroll.setCallback(function (scrollX, scrollY) { - if (scrollX < 0) { - if (scrollHandler) { - scrollHandler.scrollLeft -= 50; - } - else { - instance.view.wt.scrollHorizontal(-1).draw(); - } - } - else if (scrollX > 0) { - if (scrollHandler) { - scrollHandler.scrollLeft += 50; - } - else { - instance.view.wt.scrollHorizontal(1).draw(); - } - } - - if (scrollY < 0) { - if (scrollHandler) { - scrollHandler.scrollTop -= 20; - } - else { - instance.view.wt.scrollVertical(-1).draw(); - } - } - else if (scrollY > 0) { - if (scrollHandler) { - scrollHandler.scrollTop += 20; - } - else { - instance.view.wt.scrollVertical(1).draw(); - } - } - }); - - instance.dragToScrollListening = true; - }; - - Handsontable.hooks.add('afterInit', function () { - var instance = this; - - $(document).on('mouseup.' + this.guid, function () { - instance.dragToScrollListening = false; - }); - - $(document).on('mousemove.' + this.guid, function (event) { - if (instance.dragToScrollListening) { - dragToScroll.check(event.clientX, event.clientY); - } - }); - }); - - Handsontable.hooks.add('afterDestroy', function () { - $(document).off('.' + this.guid); - }); - - Handsontable.hooks.add('afterOnCellMouseDown', function () { - setupListening(this); - }); - - Handsontable.hooks.add('afterOnCellCornerMouseDown', function () { - setupListening(this); - }); - - Handsontable.plugins.DragToScroll = DragToScroll; -} - -(function (Handsontable, CopyPaste, SheetClip) { - - function CopyPastePlugin(instance) { - this.copyPasteInstance = CopyPaste.getInstance(); - - this.copyPasteInstance.onCut(onCut); - this.copyPasteInstance.onPaste(onPaste); - var plugin = this; - - instance.addHook('beforeKeyDown', onBeforeKeyDown); - - function onCut() { - if (!instance.isListening()) { - return; - } - - instance.selection.empty(); - } - - function onPaste(str) { - if (!instance.isListening() || !instance.selection.isSelected()) { - return; - } - - var input = str.replace(/^[\r\n]*/g, '').replace(/[\r\n]*$/g, '') //remove newline from the start and the end of the input - , inputArray = SheetClip.parse(input) - , selected = instance.getSelected() - , coordsFrom = new WalkontableCellCoords(selected[0], selected[1]) - , coordsTo = new WalkontableCellCoords(selected[2], selected[3]) - , cellRange = new WalkontableCellRange(coordsFrom, coordsFrom, coordsTo) - , topLeftCorner = cellRange.getTopLeftCorner() - , bottomRightCorner = cellRange.getBottomRightCorner() - , areaStart = topLeftCorner - , areaEnd = new WalkontableCellCoords( - Math.max(bottomRightCorner.row, inputArray.length - 1 + topLeftCorner.row), - Math.max(bottomRightCorner.col, inputArray[0].length - 1 + topLeftCorner.col) - ); - - instance.addHookOnce('afterChange', function (changes, source) { - if (changes && changes.length) { - this.selectCell(areaStart.row, areaStart.col, areaEnd.row, areaEnd.col); - } - }); - - instance.populateFromArray(areaStart.row, areaStart.col, inputArray, areaEnd.row, areaEnd.col, 'paste', instance.getSettings().pasteMode); - }; - - function onBeforeKeyDown (event) { - if (Handsontable.helper.isCtrlKey(event.keyCode) && instance.getSelected()) { - //when CTRL is pressed, prepare selectable text in textarea - //http://stackoverflow.com/questions/3902635/how-does-one-capture-a-macs-command-key-via-javascript - plugin.setCopyableText(); - event.stopImmediatePropagation(); - return; - } - - var ctrlDown = (event.ctrlKey || event.metaKey) && !event.altKey; //catch CTRL but not right ALT (which in some systems triggers ALT+CTRL) - - if (event.keyCode == Handsontable.helper.keyCode.A && ctrlDown) { - instance._registerTimeout(setTimeout(Handsontable.helper.proxy(plugin.setCopyableText, plugin), 0)); - } - - } - - this.destroy = function () { - this.copyPasteInstance.removeCallback(onCut); - this.copyPasteInstance.removeCallback(onPaste); - this.copyPasteInstance.destroy(); - instance.removeHook('beforeKeyDown', onBeforeKeyDown); - }; - - instance.addHook('afterDestroy', Handsontable.helper.proxy(this.destroy, this)); - - this.triggerPaste = Handsontable.helper.proxy(this.copyPasteInstance.triggerPaste, this.copyPasteInstance); - this.triggerCut = Handsontable.helper.proxy(this.copyPasteInstance.triggerCut, this.copyPasteInstance); - - /** - * Prepares copyable text in the invisible textarea - */ - this.setCopyableText = function () { - - var settings = instance.getSettings(); - var copyRowsLimit = settings.copyRowsLimit; - var copyColsLimit = settings.copyColsLimit; - - var selRange = instance.getSelectedRange(); - var topLeft = selRange.getTopLeftCorner(); - var bottomRight = selRange.getBottomRightCorner(); - var startRow = topLeft.row; - var startCol = topLeft.col; - var endRow = bottomRight.row; - var endCol = bottomRight.col; - var finalEndRow = Math.min(endRow, startRow + copyRowsLimit - 1); - var finalEndCol = Math.min(endCol, startCol + copyColsLimit - 1); - - instance.copyPaste.copyPasteInstance.copyable(instance.getCopyableData(startRow, startCol, finalEndRow, finalEndCol)); - - if (endRow !== finalEndRow || endCol !== finalEndCol) { - Handsontable.hooks.run(instance, "afterCopyLimit", endRow - startRow + 1, endCol - startCol + 1, copyRowsLimit, copyColsLimit); - } - }; - - } - - - - function init() { - var instance = this; - var pluginEnabled = instance.getSettings().copyPaste !== false; - - if(pluginEnabled && !instance.copyPaste){ - - instance.copyPaste = new CopyPastePlugin(instance); - - } else if (!pluginEnabled && instance.copyPaste) { - - instance.copyPaste.destroy(); - delete instance.copyPaste; - - } - - } - - Handsontable.hooks.add('afterInit', init); - Handsontable.hooks.add('afterUpdateSettings', init); - - Handsontable.hooks.register('afterCopyLimit'); -})(Handsontable, CopyPaste, SheetClip); -(function (Handsontable) { - - 'use strict'; - - Handsontable.Search = function Search(instance) { - this.query = function (queryStr, callback, queryMethod) { - var rowCount = instance.countRows(); - var colCount = instance.countCols(); - var queryResult = []; - - if (!callback) { - callback = Handsontable.Search.global.getDefaultCallback(); - } - - if (!queryMethod) { - queryMethod = Handsontable.Search.global.getDefaultQueryMethod(); - } - - for (var rowIndex = 0; rowIndex < rowCount; rowIndex++) { - for (var colIndex = 0; colIndex < colCount; colIndex++) { - var cellData = instance.getDataAtCell(rowIndex, colIndex); - var cellProperties = instance.getCellMeta(rowIndex, colIndex); - var cellCallback = cellProperties.search.callback || callback; - var cellQueryMethod = cellProperties.search.queryMethod || queryMethod; - var testResult = cellQueryMethod(queryStr, cellData); - - if (testResult) { - var singleResult = { - row: rowIndex, - col: colIndex, - data: cellData - }; - - queryResult.push(singleResult); - } - - if (cellCallback) { - cellCallback(instance, rowIndex, colIndex, cellData, testResult); - } - } - } - - return queryResult; - - }; - - }; - - Handsontable.Search.DEFAULT_CALLBACK = function (instance, row, col, data, testResult) { - instance.getCellMeta(row, col).isSearchResult = testResult; - }; - - Handsontable.Search.DEFAULT_QUERY_METHOD = function (query, value) { - - if (typeof query == 'undefined' || query == null || !query.toLowerCase || query.length == 0){ - return false; - } - - if(typeof value == 'undefined' || value == null) { - return false; - } - - return value.toString().toLowerCase().indexOf(query.toLowerCase()) != -1; - }; - - Handsontable.Search.DEFAULT_SEARCH_RESULT_CLASS = 'htSearchResult'; - - Handsontable.Search.global = (function () { - - var defaultCallback = Handsontable.Search.DEFAULT_CALLBACK; - var defaultQueryMethod = Handsontable.Search.DEFAULT_QUERY_METHOD; - var defaultSearchResultClass = Handsontable.Search.DEFAULT_SEARCH_RESULT_CLASS; - - return { - getDefaultCallback: function () { - return defaultCallback; - }, - - setDefaultCallback: function (newDefaultCallback) { - defaultCallback = newDefaultCallback; - }, - - getDefaultQueryMethod: function () { - return defaultQueryMethod; - }, - - setDefaultQueryMethod: function (newDefaultQueryMethod) { - defaultQueryMethod = newDefaultQueryMethod; - }, - - getDefaultSearchResultClass: function () { - return defaultSearchResultClass; - }, - - setDefaultSearchResultClass: function (newSearchResultClass) { - defaultSearchResultClass = newSearchResultClass; - } - } - - })(); - - - - Handsontable.SearchCellDecorator = function (instance, TD, row, col, prop, value, cellProperties) { - - var searchResultClass = (typeof cellProperties.search == 'object' && cellProperties.search.searchResultClass) || Handsontable.Search.global.getDefaultSearchResultClass(); - - if(cellProperties.isSearchResult){ - Handsontable.Dom.addClass(TD, searchResultClass); - } else { - Handsontable.Dom.removeClass(TD, searchResultClass); - } - }; - - - - var originalDecorator = Handsontable.renderers.cellDecorator; - - Handsontable.renderers.cellDecorator = function (instance, TD, row, col, prop, value, cellProperties) { - originalDecorator.apply(this, arguments); - Handsontable.SearchCellDecorator.apply(this, arguments); - }; - - function init() { - var instance = this; - - var pluginEnabled = !!instance.getSettings().search; - - if (pluginEnabled) { - instance.search = new Handsontable.Search(instance); - } else { - delete instance.search; - } - - } - - Handsontable.hooks.add('afterInit', init); - Handsontable.hooks.add('afterUpdateSettings', init); - - -})(Handsontable); -function CellInfoCollection() { - - var collection = []; - - collection.getInfo = function (row, col) { - for (var i = 0, ilen = this.length; i < ilen; i++) { - if (this[i].row <= row && this[i].row + this[i].rowspan - 1 >= row && this[i].col <= col && this[i].col + this[i].colspan - 1 >= col) { - return this[i]; - } - } - }; - - collection.setInfo = function (info) { - for (var i = 0, ilen = this.length; i < ilen; i++) { - if (this[i].row === info.row && this[i].col === info.col) { - this[i] = info; - return; - } - } - this.push(info); - }; - - collection.removeInfo = function (row, col) { - for (var i = 0, ilen = this.length; i < ilen; i++) { - if (this[i].row === row && this[i].col === col) { - this.splice(i, 1); - break; - } - } - }; - - return collection; - -} - - -/** - * Plugin used to merge cells in Handsontable - * @constructor - */ -function MergeCells(mergeCellsSetting) { - this.mergedCellInfoCollection = new CellInfoCollection(); - - if (Handsontable.helper.isArray(mergeCellsSetting)) { - for (var i = 0, ilen = mergeCellsSetting.length; i < ilen; i++) { - this.mergedCellInfoCollection.setInfo(mergeCellsSetting[i]); - } - } -} - -/** - * @param cellRange (WalkontableCellRange) - */ -MergeCells.prototype.canMergeRange = function (cellRange) { - //is more than one cell selected - return !cellRange.isSingle(); -}; - -MergeCells.prototype.mergeRange = function (cellRange) { - if (!this.canMergeRange(cellRange)) { - return; - } - - //normalize top left corner - var topLeft = cellRange.getTopLeftCorner(); - var bottomRight = cellRange.getBottomRightCorner(); - - var mergeParent = {}; - mergeParent.row = topLeft.row; - mergeParent.col = topLeft.col; - mergeParent.rowspan = bottomRight.row - topLeft.row + 1; //TD has rowspan == 1 by default. rowspan == 2 means spread over 2 cells - mergeParent.colspan = bottomRight.col - topLeft.col + 1; - this.mergedCellInfoCollection.setInfo(mergeParent); -}; - -MergeCells.prototype.mergeOrUnmergeSelection = function (cellRange) { - var info = this.mergedCellInfoCollection.getInfo(cellRange.from.row, cellRange.from.col); - if (info) { - //unmerge - this.unmergeSelection(cellRange.from); - } - else { - //merge - this.mergeSelection(cellRange); - } -}; - -MergeCells.prototype.mergeSelection = function (cellRange) { - this.mergeRange(cellRange); -}; - -MergeCells.prototype.unmergeSelection = function (cellRange) { - var info = this.mergedCellInfoCollection.getInfo(cellRange.row, cellRange.col); - this.mergedCellInfoCollection.removeInfo(info.row, info.col); -}; - -MergeCells.prototype.applySpanProperties = function (TD, row, col) { - var info = this.mergedCellInfoCollection.getInfo(row, col); - if (info) { - if (info.row === row && info.col === col) { - TD.setAttribute('rowspan', info.rowspan); - TD.setAttribute('colspan', info.colspan); - } - else { - TD.style.display = "none"; - } - } - else { - TD.removeAttribute('rowspan'); - TD.removeAttribute('colspan'); - } -}; - -MergeCells.prototype.modifyTransform = function (hook, currentSelectedRange, delta) { - var current; - switch (hook) { - case 'modifyTransformStart': - current = currentSelectedRange.highlight; - break; - - case 'modifyTransformEnd': - current = currentSelectedRange.to; - break; - } - - if (hook == "modifyTransformStart") { - //in future - can this take the logic from modifyTransformEnd? - var mergeParent = this.mergedCellInfoCollection.getInfo(current.row + delta.row, current.col + delta.col); - if (mergeParent) { - if (current.row > mergeParent.row) { //entering merge by going up or left - this.lastDesiredCoords = new WalkontableCellCoords(current.row + delta.row, current.col + delta.col); //copy - delta.row += (mergeParent.row - current.row) - delta.row; - } - else if (current.row == mergeParent.row && delta.row > 0) { //leaving merge by going down - delta.row += mergeParent.row - current.row + mergeParent.rowspan - 1; - } - else { //leaving merge by going right - if (this.lastDesiredCoords && delta.row === 0) { - delta.row += this.lastDesiredCoords.row - current.row; - this.lastDesiredCoords = null; - } - } - - if (current.col > mergeParent.col) { //entering merge by going up or left - if (!this.lastDesiredCoords) { - this.lastDesiredCoords = new WalkontableCellCoords(current.row + delta.row, current.col + delta.col); //copy - } - delta.col += (mergeParent.col - current.col) - delta.col; - } - else if (current.col == mergeParent.col && delta.col > 0) { //leaving merge by going right - delta.col += mergeParent.col - current.col + mergeParent.colspan - 1; - } - else { //leaving merge by going down - if (this.lastDesiredCoords && delta.col === 0) { - delta.col += this.lastDesiredCoords.col - current.col; - this.lastDesiredCoords = null; - } - } - } - else { - if (this.lastDesiredCoords) { - if (delta.col == 0) { //leaving merge by going up - delta.col += this.lastDesiredCoords.col - current.col; - } - else if (delta.row == 0) { //leaving merge by going left - delta.row += this.lastDesiredCoords.row - current.row; - } - this.lastDesiredCoords = null; - } - } - } - else { - //modify transform end - var hightlightMergeParent = this.mergedCellInfoCollection.getInfo(currentSelectedRange.highlight.row, currentSelectedRange.highlight.col); - if (hightlightMergeParent) { - if (currentSelectedRange.isSingle()) { - currentSelectedRange.from = new WalkontableCellCoords(hightlightMergeParent.row, hightlightMergeParent.col); - currentSelectedRange.to = new WalkontableCellCoords(hightlightMergeParent.row + hightlightMergeParent.rowspan - 1, hightlightMergeParent.col + hightlightMergeParent.colspan - 1); - } - } - - if (currentSelectedRange.isSingle()) { - //make sure objects are clones but not reference to the same instance - //because we will mutate them - currentSelectedRange.from = new WalkontableCellCoords(currentSelectedRange.highlight.row, currentSelectedRange.highlight.col); - currentSelectedRange.to = new WalkontableCellCoords(currentSelectedRange.highlight.row, currentSelectedRange.highlight.col); - } - - var solveDimension = function (dim) { - var altDim = dim == "col" ? "row" : "col"; - - function changeCoords(obj, altDimValue, dimValue) { - obj[altDim] = altDimValue; - obj[dim] = dimValue; - } - - if (delta[dim] != 0) { - var topLeft; - var bottomRight; - - var updateCornerInfo = function () { - topLeft = currentSelectedRange.getTopLeftCorner(); - bottomRight = currentSelectedRange.getBottomRightCorner(); - } - updateCornerInfo(); - - var expanding = false; //expanding false means shrinking - var examinedCol; - //now check if maybe we are expanding? - if (delta[dim] < 0) { - examinedCol = bottomRight[dim] + delta[dim]; - if (bottomRight[dim] == currentSelectedRange.highlight[dim]) { - examinedCol = topLeft[dim] + delta[dim]; - expanding = true; - } - else { - for (var i = topLeft[altDim]; i <= bottomRight[altDim]; i++) { - var mergeParent = this.mergedCellInfoCollection.getInfo(i, bottomRight[dim]); - if (mergeParent) { - if (mergeParent[dim] <= currentSelectedRange.highlight[dim]) { - examinedCol = topLeft[dim] + delta[dim]; - expanding = true; - break; - } - } - } - } - } - else if (delta[dim] > 0) { - examinedCol = topLeft[dim] + delta[dim]; - if (topLeft[dim] == currentSelectedRange.highlight[dim]) { - examinedCol = bottomRight[dim] + delta[dim]; - expanding = true; - } - else { - for (var i = topLeft[altDim]; i <= bottomRight[altDim]; i++) { - var mergeParent = this.mergedCellInfoCollection.getInfo(i, topLeft[dim]); - if (mergeParent) { - if (mergeParent[dim] + mergeParent[dim + "span"] > currentSelectedRange.highlight[dim]) { - examinedCol = bottomRight[dim] + delta[dim]; - expanding = true; - break; - } - } - } - } - } - - if (expanding) { - if (delta[dim] > 0) { //moving East wall further East - changeCoords(currentSelectedRange.from, topLeft[altDim], topLeft[dim]); - changeCoords(currentSelectedRange.to, bottomRight[altDim], Math.max(bottomRight[dim], examinedCol)); - updateCornerInfo(); - } - else { //moving West wall further West - changeCoords(currentSelectedRange.from, topLeft[altDim], Math.min(topLeft[dim], examinedCol)); - changeCoords(currentSelectedRange.to, bottomRight[altDim], bottomRight[dim]); - updateCornerInfo(); - } - - } - else { - if (delta[dim] > 0) { //shrinking West wall towards East - changeCoords(currentSelectedRange.from, topLeft[altDim], Math.max(topLeft[dim], examinedCol)); - changeCoords(currentSelectedRange.to, bottomRight[altDim], bottomRight[dim]); - updateCornerInfo(); - } - else { //shrinking East wall towards West - changeCoords(currentSelectedRange.from, topLeft[altDim], topLeft[dim]); - changeCoords(currentSelectedRange.to, bottomRight[altDim], Math.min(bottomRight[dim], examinedCol)); - updateCornerInfo(); - } - } - - for (var i = topLeft[altDim]; i <= bottomRight[altDim]; i++) { - var mergeParent = dim == "col" ? this.mergedCellInfoCollection.getInfo(i, examinedCol) : this.mergedCellInfoCollection.getInfo(examinedCol, i); - if (mergeParent) { - if (expanding) { - if (delta[dim] > 0) { //moving East wall further East - changeCoords(currentSelectedRange.from, Math.min(topLeft[altDim], mergeParent[altDim]), Math.min(topLeft[dim], mergeParent[dim])); - if (examinedCol > mergeParent[dim]) { - changeCoords(currentSelectedRange.to, Math.max(bottomRight[altDim], mergeParent[altDim] + mergeParent[altDim + "span"] - 1), Math.max(bottomRight[dim], mergeParent[dim] + mergeParent[dim + "span"])); - } - else { - changeCoords(currentSelectedRange.to, Math.max(bottomRight[altDim], mergeParent[altDim] + mergeParent[altDim + "span"] - 1), Math.max(bottomRight[dim], mergeParent[dim] + mergeParent[dim + "span"] - 1)); - } - updateCornerInfo(); - } - else { //moving West wall further West - changeCoords(currentSelectedRange.from, Math.min(topLeft[altDim], mergeParent[altDim]), Math.min(topLeft[dim], mergeParent[dim])); - changeCoords(currentSelectedRange.to, Math.max(bottomRight[altDim], mergeParent[altDim] + mergeParent[altDim + "span"] - 1), Math.max(bottomRight[dim], mergeParent[dim] + mergeParent[dim + "span"] - 1)); - updateCornerInfo(); - } - } - else { - if (delta[dim] > 0) { //shrinking West wall towards East - if (examinedCol > mergeParent[dim]) { - changeCoords(currentSelectedRange.from, topLeft[altDim], Math.max(topLeft[dim], mergeParent[dim] + mergeParent[dim + "span"])); - changeCoords(currentSelectedRange.to, bottomRight[altDim], Math.max(bottomRight[dim], mergeParent[dim] + mergeParent[dim + "span"])); - } - else { - changeCoords(currentSelectedRange.from, topLeft[altDim], Math.max(topLeft[dim], mergeParent[dim])); - changeCoords(currentSelectedRange.to, bottomRight[altDim], Math.max(bottomRight[dim], mergeParent[dim] + mergeParent[dim + "span"] - 1)); - } - updateCornerInfo(); - } - else { //shrinking East wall towards West - if (examinedCol < mergeParent[dim] + mergeParent[dim + "span"] - 1) { - changeCoords(currentSelectedRange.from, topLeft[altDim], Math.min(topLeft[dim], mergeParent[dim] - 1)); - changeCoords(currentSelectedRange.to, bottomRight[altDim], Math.min(bottomRight[dim], mergeParent[dim] - 1)); - } - else { - changeCoords(currentSelectedRange.from, topLeft[altDim], Math.min(topLeft[dim], mergeParent[dim])); - changeCoords(currentSelectedRange.to, bottomRight[altDim], Math.min(bottomRight[dim], mergeParent[dim] + mergeParent[dim + "span"])); - } - updateCornerInfo(); - } - } - } - } - - /*if (expanding) { - //check if corners are not part of merged cells as well - var oneLastCheck = function (row, col) { - var mergeParent = this.mergedCellInfoCollection.getInfo(row, col); - if (mergeParent) { - currentSelectedRange.expand(new WalkontableCellCoords(mergeParent.row, mergeParent.col)); - currentSelectedRange.expand(new WalkontableCellCoords(mergeParent.row + mergeParent.rowspan - 1, mergeParent.col + mergeParent.colspan - 1)); - updateCornerInfo(); - } - } - oneLastCheck.call(this, topLeft.row, topLeft.col); - oneLastCheck.call(this, topLeft.row, bottomRight.col); - oneLastCheck.call(this, bottomRight.row, bottomRight.col); - oneLastCheck.call(this, bottomRight.row, topLeft.col); - } - else { - //TODO there is still a glitch if you go to merge_cells.html, go to D5 and press up, right, down - }*/ - } - }; - - solveDimension.call(this, "col"); - solveDimension.call(this, "row"); - - delta.row = 0; - delta.col = 0; - } -}; - -if (typeof Handsontable == 'undefined') { - throw new Error('Handsontable is not defined'); -} - -var init = function () { - var instance = this; - var mergeCellsSetting = instance.getSettings().mergeCells; - - if (mergeCellsSetting) { - if (!instance.mergeCells) { - instance.mergeCells = new MergeCells(mergeCellsSetting); - } - } -}; - -var onBeforeKeyDown = function (event) { - if (!this.mergeCells) { - return; - } - - var ctrlDown = (event.ctrlKey || event.metaKey) && !event.altKey; - - if (ctrlDown) { - if (event.keyCode === 77) { //CTRL + M - this.mergeCells.mergeOrUnmergeSelection(this.getSelectedRange()); - this.render(); - event.stopImmediatePropagation(); - } - } -}; - -var addMergeActionsToContextMenu = function (defaultOptions) { - if (!this.getSettings().mergeCells) { - return; - } - - defaultOptions.items.mergeCellsSeparator = Handsontable.ContextMenu.SEPARATOR; - - defaultOptions.items.mergeCells = { - name: function () { - var sel = this.getSelected(); - var info = this.mergeCells.mergedCellInfoCollection.getInfo(sel[0], sel[1]); - if (info) { - return 'Unmerge cells'; - } - else { - return 'Merge cells'; - } - }, - callback: function () { - this.mergeCells.mergeOrUnmergeSelection(this.getSelectedRange()); - this.render(); - }, - disabled: function () { - return false; - } - }; -}; - -var afterRenderer = function (TD, row, col, prop, value, cellProperties) { - if (this.mergeCells) { - this.mergeCells.applySpanProperties(TD, row, col); - } -}; - -var modifyTransformFactory = function (hook) { - return function (delta) { - var mergeCellsSetting = this.getSettings().mergeCells; - if (mergeCellsSetting) { - var currentSelectedRange = this.getSelectedRange(); - this.mergeCells.modifyTransform(hook, currentSelectedRange, delta); - - if (hook === "modifyTransformEnd") { - //sanitize "from" (core.js will sanitize to) - var totalRows = this.countRows(); - var totalCols = this.countCols(); - if (currentSelectedRange.from.row < 0) { - currentSelectedRange.from.row = 0; - } - else if (currentSelectedRange.from.row > 0 && currentSelectedRange.from.row >= totalRows) { - currentSelectedRange.from.row = currentSelectedRange.from - 1; - } - - if (currentSelectedRange.from.col < 0) { - currentSelectedRange.from.col = 0; - } - else if (currentSelectedRange.from.col > 0 && currentSelectedRange.from.col >= totalCols) { - currentSelectedRange.from.col = totalCols - 1; - } - } - } - } -}; - -/** - * While selecting cells with keyboard or mouse, make sure that rectangular area is expanded to the extent of the merged cell - * @param coords - */ -var beforeSetRangeEnd = function (coords) { - this.lastDesiredCoords = null; //unset lastDesiredCoords when selection is changed with mouse - var mergeCellsSetting = this.getSettings().mergeCells; - if (mergeCellsSetting) { - var selRange = this.getSelectedRange(); - selRange.highlight = new WalkontableCellCoords(selRange.highlight.row, selRange.highlight.col); //clone in case we will modify its reference - selRange.to = coords; - - for (var i = 0, ilen = this.mergeCells.mergedCellInfoCollection.length; i < ilen; i++) { - var cellInfo = this.mergeCells.mergedCellInfoCollection[i]; - var mergedCellTopLeft = new WalkontableCellCoords(cellInfo.row, cellInfo.col); - var mergedCellBottomRight = new WalkontableCellCoords(cellInfo.row + cellInfo.rowspan - 1, cellInfo.col + cellInfo.colspan - 1); - - var mergedCellRange = new WalkontableCellRange(mergedCellTopLeft, mergedCellTopLeft, mergedCellBottomRight); - if (selRange.expandByRange(mergedCellRange)) { - var selRangeBottomRight = selRange.getBottomRightCorner(); - coords.row = selRangeBottomRight.row; - coords.col = selRangeBottomRight.col; - } - } - } -}; - -var afterGetCellMeta = function(row, col, cellProperties) { - var mergeCellsSetting = this.getSettings().mergeCells; - if (mergeCellsSetting) { - var mergeParent = this.mergeCells.mergedCellInfoCollection.getInfo(row, col); - if(mergeParent && (mergeParent.row != row || mergeParent.col != col)) { - cellProperties.copyable = false; - } - } -}; - -Handsontable.hooks.add('beforeInit', init); -Handsontable.hooks.add('beforeKeyDown', onBeforeKeyDown); -Handsontable.hooks.add('modifyTransformStart', modifyTransformFactory('modifyTransformStart')); -Handsontable.hooks.add('modifyTransformEnd', modifyTransformFactory('modifyTransformEnd')); -Handsontable.hooks.add('beforeSetRangeEnd', beforeSetRangeEnd); -Handsontable.hooks.add('afterRenderer', afterRenderer); -Handsontable.hooks.add('afterContextMenuDefaultOptions', addMergeActionsToContextMenu); -Handsontable.hooks.add('afterGetCellMeta', afterGetCellMeta); - -Handsontable.MergeCells = MergeCells; - - -(function () { - - function CustomBorders () { - - } - - /*** - * Array for all custom border objects (for redraw) - * @type {{}} - */ - var bordersArray = {}, - /*** - * Flag for prevent redraw borders after each AfterRender hook - * @type {boolean} - */ - initialDraw = false, - - /*** - * Current instance (table where borders should be placed) - */ - instance; - - - /*** - * Check if plugin should be enabled - */ - var init = function () { - - var customBorders = this.getSettings().customBorders; - var enable = false; - - if(typeof customBorders === "boolean"){ - if (customBorders == true){ - enable = true; - } - } - - if(typeof customBorders === "object"){ - if(customBorders.length > 0) { - initialDraw = true; - enable = true; - } - } - - if(enable){ - if(!this.customBorders){ - instance = this; - this.customBorders = new CustomBorders(); - } - } - }; - - /*** - * Prepare borders from setting (single cell) - * - * @param row - * @param col - * @param borderObj - */ - var prepareBorderFromCustomAdded = function (row, col, borderObj){ - var border = createEmptyBorders(row, col); - border = extendDefaultBorder(border, borderObj); - this.setCellMeta(row, col, 'borders', border); - insertBorderToArray(border); - }; - - /*** - * Prepare borders from setting (object) - * @param rowObj - */ - var prepareBorderFromCustomAddedRange = function (rowObj) { - var range = rowObj.range; - - for (var row = range.from.row; row <= range.to.row; row ++) { - for (var col = range.from.col; col<= range.to.col; col++){ - - var border = createEmptyBorders(row, col); - var add = 0; - - if(row == range.from.row) { - add++; - if(rowObj.hasOwnProperty('top')){ - border.top = rowObj.top; - } - } - - if(row == range.to.row){ - add++; - if(rowObj.hasOwnProperty('bottom')){ - border.bottom = rowObj.bottom; - } - } - - if(col == range.from.col) { - add++; - if(rowObj.hasOwnProperty('left')){ - border.left = rowObj.left; - } - } - - - if (col == range.to.col) { - add++; - if(rowObj.hasOwnProperty('right')){ - border.right = rowObj.right; - } - } - - - if(add>0){ - this.setCellMeta(row, col, 'borders', border); - insertBorderToArray(border); - } - } - } - }; - - /*** - * Create separated class name for borders for each cell - * @param row - * @param col - * @returns {string} - */ - var createClassName = function (row, col) { - return "border_row" + row + "col" + col; - }; - - - /*** - * Create default single border for each position (top/right/bottom/left) - * @returns {{width: number, color: string}} - */ - var createDefaultCustomBorder = function () { - return { - width: 1, - color: '#000' - }; - }; - - - /*** - * Create default object for empty border - * @returns {{hide: boolean}} - */ - var createSingleEmptyBorder = function () { - return { - hide: true - } - }; - - - /*** - * Create default Handsontable border object - * @returns {{width: number, color: string, cornerVisible: boolean}} - */ - var createDefaultHtBorder = function () { - return { - width: 1, - color: '#000', - cornerVisible: false - } - }; - - /*** - * Prepare empty border for each cell with all custom borders hidden - * - * @param row - * @param col - * @returns {{className: *, border: *, row: *, col: *, top: {hide: boolean}, right: {hide: boolean}, bottom: {hide: boolean}, left: {hide: boolean}}} - */ - var createEmptyBorders = function (row, col){ - return { - className: createClassName(row, col), - border: createDefaultHtBorder(), - row: row, - col: col, - top: createSingleEmptyBorder(), - right: createSingleEmptyBorder(), - bottom: createSingleEmptyBorder(), - left: createSingleEmptyBorder() - } - }; - - - var extendDefaultBorder = function (defaultBorder, customBorder){ - - if(customBorder.hasOwnProperty('border')){ - defaultBorder.border = customBorder.border; - } - - if(customBorder.hasOwnProperty('top')){ - defaultBorder.top = customBorder.top; - } - - if(customBorder.hasOwnProperty('right')){ - defaultBorder.right = customBorder.right; - } - - if(customBorder.hasOwnProperty('bottom')){ - defaultBorder.bottom = customBorder.bottom; - } - - if(customBorder.hasOwnProperty('left')){ - defaultBorder.left = customBorder.left; - } - return defaultBorder; - }; - - /*** - * Insert object with borders for each cell to bordersArray - * - * @param bordersObj - */ - var insertBorderToArray = function (bordersObj) { - bordersArray[bordersObj.className] = bordersObj; - }; - - /*** - * Clean bordersArray for cell when custom border has been removed - * - * @param className - */ - var removeBorderFromArray = function (className) { - delete bordersArray[className]; - }; - - - /*** - * Remove borders divs from DOM - * - * @param borderClassName - */ - var removeBordersFromDom = function (borderClassName) { - var borders = document.getElementsByClassName(borderClassName)[0]; - - if(borders){ - var parent = borders.parentNode; - parent.parentNode.removeChild(parent); - } - - }; - - - /*** - * Remove border (triggered from context menu) - * - * @param row - * @param col - */ - var removeAllBorders = function(row,col) { - var borderClassName = createClassName(row,col); - removeBordersFromDom(borderClassName); - removeBorderFromArray(borderClassName); - - this.removeCellMeta(row, col, 'borders'); - }; - - /*** - * Draw borders for single cell - * - * @param borderObj - */ - var drawBorders = function (borderObj) { - var bordersInDOM = document.getElementsByClassName(createClassName(borderObj.row,borderObj.col)), - bordersExist = bordersInDOM.length > 0; - - if(bordersExist){ - removeBordersFromDom(createClassName(borderObj.row,borderObj.col)); - } - - var border = new WalkontableBorder(this.view.wt,borderObj); - border.appear([borderObj.row,borderObj.col,borderObj.row,borderObj.col]); - }; - - - /*** - * Set borders for each cell re. to border position - * - * @param row - * @param col - * @param place - * @param remove - */ - var setBorder = function (row, col,place, remove){ - var bordersMeta = this.getCellMeta(row, col).borders; - if (!bordersMeta || bordersMeta.border == undefined){ - bordersMeta = createEmptyBorders(row, col); - } - - if (remove) { - bordersMeta[place] = createSingleEmptyBorder(); - } else { - bordersMeta[place] = createDefaultCustomBorder(); - } - - - this.setCellMeta(row, col, 'borders', bordersMeta); - insertBorderToArray(bordersMeta); -// doDraw = true; - this.render(); - }; - - - /*** - * Prepare borders based on cell and border position - * - * @param range - * @param place - * @param remove - */ - var prepareBorder = function (range, place, remove) { - if (range.from.row == range.to.row && range.from.col == range.to.col){ - if(place == "noBorders"){ - removeAllBorders.call(this, range.from.row, range.from.col); - } else { - setBorder.call(this, range.from.row, range.from.col, place, remove); - } - } else { - switch (place) { - case "noBorders": - for(var column = range.from.col; column <= range.to.col; column++){ - for(var row = range.from.row; row <= range.to.row; row++) { - removeAllBorders.call(this, row, column); - } - } - break; - case "top": - for(var topCol = range.from.col; topCol <= range.to.col; topCol++){ - setBorder.call(this, range.from.row, topCol, place, remove); - } - break; - case "right": - for(var rowRight = range.from.row; rowRight <=range.to.row; rowRight++){ - setBorder.call(this,rowRight, range.to.col, place); - } - break; - case "bottom": - for(var bottomCol = range.from.col; bottomCol <= range.to.col; bottomCol++){ - setBorder.call(this, range.to.row, bottomCol, place); - } - break; - case "left": - for(var rowLeft = range.from.row; rowLeft <=range.to.row; rowLeft++){ - setBorder.call(this,rowLeft, range.from.col, place); - } - break; - } - } - }; - - /*** - * Check if selection has border by className - * - * @param hot - * @param direction - */ - var checkSelectionBorders = function (hot, direction) { - var atLeastOneHasBorder = false; - - hot.getSelectedRange().forAll(function(r, c) { - var metaBorders = hot.getCellMeta(r,c).borders; - - if (metaBorders) { - if(direction) { - if (!metaBorders[direction].hasOwnProperty('hide')){ - atLeastOneHasBorder = true; - return false; //breaks forAll - } - } else { - atLeastOneHasBorder = true; - return false; //breaks forAll - } - } - }); - return atLeastOneHasBorder; - }; - - - /*** - * Mark label in contextMenu as selected - * - * @param label - * @returns {string} - */ - var markSelected = function (label) { - return "" + label; - }; - - /*** - * Add border options to context menu - * - * @param defaultOptions - */ - var addBordersOptionsToContextMenu = function (defaultOptions) { - if(!this.getSettings().customBorders){ - return; - } - - defaultOptions.items.bordersCellsSeparator = Handsontable.ContextMenu.SEPARATOR; - - defaultOptions.items.borders = { - name: 'Borders', - submenu: { - items: { - top: { - name: function () { - var label = "Top"; - var hasBorder = checkSelectionBorders(this, 'top'); - if(hasBorder) { - label = markSelected(label); - } - - return label; - }, - callback: function () { - var hasBorder = checkSelectionBorders(this, 'top'); - prepareBorder.call(this, this.getSelectedRange(), 'top', hasBorder); - }, - disabled: false - }, - right: { - name: function () { - var label = 'Right'; - var hasBorder = checkSelectionBorders(this, 'right'); - if(hasBorder) { - label = markSelected(label); - } - return label; - }, - callback: function () { - var hasBorder = checkSelectionBorders(this, 'right'); - prepareBorder.call(this, this.getSelectedRange(), 'right', hasBorder); - }, - disabled: false - }, - bottom: { - name: function () { - var label = 'Bottom'; - var hasBorder = checkSelectionBorders(this, 'bottom'); - if(hasBorder) { - label = markSelected(label); - } - return label; - }, - callback: function () { - var hasBorder = checkSelectionBorders(this, 'bottom'); - prepareBorder.call(this, this.getSelectedRange(), 'bottom', hasBorder); - }, - disabled: false - }, - left: { - name: function () { - var label = 'Left'; - var hasBorder = checkSelectionBorders(this, 'left'); - if(hasBorder) { - label = markSelected(label); - } - - return label - }, - callback: function () { - var hasBorder = checkSelectionBorders(this, 'bottom'); - prepareBorder.call(this, this.getSelectedRange(), 'left', hasBorder); - }, - disabled: false - }, - remove: { - name: 'Remove border(s)', - callback: function () { - prepareBorder.call(this, this.getSelectedRange(), 'noBorders'); - }, - disabled: function () { - return !checkSelectionBorders(this); - } - } - } - } - }; - }; - - Handsontable.hooks.add('beforeInit', init); - Handsontable.hooks.add('afterContextMenuDefaultOptions', addBordersOptionsToContextMenu); - Handsontable.hooks.add('afterRender', function () { - var customBorders = this.getSettings().customBorders; - - if (initialDraw){ - for(var i = 0; i< customBorders.length; i++) { - if(customBorders[i].range){ - prepareBorderFromCustomAddedRange.call(this,customBorders[i]); - } else { - prepareBorderFromCustomAdded.call(this,customBorders[i].row, customBorders[i].col, customBorders[i]); - } - } - initialDraw = false; - } - - for (var key in bordersArray) { - if (bordersArray.hasOwnProperty(key)) { - - drawBorders.call(this,bordersArray[key]) - } - } - - }); - Handsontable.CustomBorders = CustomBorders; - -}()); - -/** - * HandsontableManualRowMove - * - * Has 2 UI components: - * - handle - the draggable element that sets the desired position of the row - * - guide - the helper guide that shows the desired position as a horizontal guide - * - * Warning! Whenever you make a change in this file, make an analogous change in manualRowMove.js - * @constructor - */ -(function (Handsontable) { - function HandsontableManualRowMove() { - - var startRow, - endRow, - startY, - startOffset, - currentRow, - currentTH, - handle = document.createElement('DIV'), - guide = document.createElement('DIV'), - $window = $(window); - - handle.className = 'manualRowMover'; - guide.className = 'manualRowMoverGuide'; - - var saveManualRowPositions = function () { - var instance = this; - Handsontable.hooks.run(instance, 'persistentStateSave', 'manualRowPositions', instance.manualRowPositions); - }; - - var loadManualRowPositions = function () { - var instance = this, - storedState = {}; - Handsontable.hooks.run(instance, 'persistentStateLoad', 'manualRowPositions', storedState); - return storedState.value; - }; - - function setupHandlePosition(TH) { - instance = this; - currentTH = TH; - - var row = this.view.wt.wtTable.getCoords(TH).row; //getCoords returns WalkontableCellCoords - if (row >= 0) { //if not row header - currentRow = row; - var box = currentTH.getBoundingClientRect(); - startOffset = box.top; - handle.style.top = startOffset + 'px'; - handle.style.left = box.left + 'px'; - instance.rootElement[0].appendChild(handle); - } - } - - function refreshHandlePosition(TH) { - var box = TH.getBoundingClientRect(); - handle.style.top = box.top + 'px'; - } - - function setupGuidePosition() { - var instance = this; - Handsontable.Dom.addClass(handle, 'active'); - Handsontable.Dom.addClass(guide, 'active'); - var box = currentTH.getBoundingClientRect(); - guide.style.width = instance.view.maximumVisibleElementWidth(0) + 'px'; - guide.style.height = box.height + 'px'; - guide.style.top = startOffset + 'px'; - guide.style.left = handle.style.left; - instance.rootElement[0].appendChild(guide); - } - - function refreshGuidePosition(diff) { - guide.style.top = startOffset + diff + 'px'; - } - - function hideHandleAndGuide() { - Handsontable.Dom.removeClass(handle, 'active'); - Handsontable.Dom.removeClass(guide, 'active'); - } - - var bindEvents = function () { - var instance = this; - var pressed; - - instance.rootElement.on('mouseenter.manualRowMove.' + instance.guid, 'table tbody tr > th', function (e) { - if (pressed) { - endRow = instance.view.wt.wtTable.getCoords(e.currentTarget).row; - refreshHandlePosition(e.currentTarget); - } - else { - setupHandlePosition.call(instance, e.currentTarget); - } - }); - - instance.rootElement.on('mousedown.manualRowMove.' + instance.guid, '.manualRowMover', function (e) { - startY = e.pageY; - setupGuidePosition.call(instance); - pressed = instance; - - startRow = currentRow; - endRow = currentRow; - }); - - $window.on('mousemove.manualRowMove.' + instance.guid, function (e) { - if (pressed) { - refreshGuidePosition(e.pageY - startY); - } - }); - - $window.on('mouseup.manualRowMove.' + instance.guid, function () { - if (pressed) { - hideHandleAndGuide(); - pressed = false; - - if (startRow < endRow) { - endRow--; - } - createPositionData(instance.manualRowPositions, instance.countRows()); - instance.manualRowPositions.splice(endRow, 0, instance.manualRowPositions.splice(startRow, 1)[0]); - - instance.forceFullRender = true; - instance.view.render(); //updates all - - saveManualRowPositions.call(instance); - - Handsontable.hooks.run(instance, 'afterRowMove', startRow, endRow); - - setupHandlePosition.call(instance, currentTH); - } - }); - - instance.addHook('afterDestroy', unbindEvents); - }; - - var unbindEvents = function () { - var instance = this; - instance.rootElement.off('mouseenter.manualRowMove.' + instance.guid, 'table tbody tr > th'); - instance.rootElement.off('mousedown.manualRowMove.' + instance.guid, '.manualRowMover'); - $window.off('mousemove.manualRowMove.' + instance.guid); - $window.off('mouseup.manualRowMove.' + instance.guid); - }; - - var createPositionData = function (positionArr, len) { - if (positionArr.length < len) { - for (var i = positionArr.length; i < len; i++) { - positionArr[i] = i; - } - } - }; - - this.beforeInit = function () { - this.manualRowPositions = []; - }; - - this.init = function (source) { - var instance = this; - - var manualRowMoveEnabled = !!(instance.getSettings().manualRowMove); - - if (manualRowMoveEnabled) { - var initialManualRowPositions = instance.getSettings().manualRowMove; - - var loadedManualRowPostions = loadManualRowPositions.call(instance); - - if (typeof loadedManualRowPostions != 'undefined') { - this.manualRowPositions = loadedManualRowPostions; - } else if(initialManualRowPositions instanceof Array) { - this.manualRowPositions = initialManualRowPositions; - } else { - this.manualRowPositions = []; - } - - if (source === 'afterInit') { - bindEvents.call(this); - if (this.manualRowPositions.length > 0) { - instance.forceFullRender = true; - instance.render(); - } - } - } else { - unbindEvents.call(this); - instance.manualRowPositions = []; - } - - }; - - this.modifyRow = function (row) { - var instance = this; - if (instance.getSettings().manualRowMove) { - if (typeof instance.manualRowPositions[row] === 'undefined') { - createPositionData(this.manualRowPositions, row + 1); - } - return instance.manualRowPositions[row]; - } - - return row; - }; - } - - var htManualRowMove = new HandsontableManualRowMove(); - - Handsontable.hooks.add('beforeInit', htManualRowMove.beforeInit); - Handsontable.hooks.add('afterInit', function () { - htManualRowMove.init.call(this, 'afterInit'); - }); - - Handsontable.hooks.add('afterUpdateSettings', function () { - htManualRowMove.init.call(this, 'afterUpdateSettings'); - }); - - Handsontable.hooks.add('modifyRow', htManualRowMove.modifyRow); - Handsontable.hooks.register('afterRowMove'); - -})(Handsontable); -/** - * This plugin provides "drag-down" and "copy-down" functionalities, both operated - * using the small square in the right bottom of the cell selection. - * - * "Drag-down" expands the value of the selected cells to the neighbouring - * cells when you drag the small square in the corner. - * - * "Copy-down" copies the value of the selection to all empty cells - * below when you double click the small square. - */ -(function (Handsontable) { - 'use strict'; - - function Autofill(instance) { - this.instance = instance; - this.addingStarted = false; - - var $document = $(document), - wtOnCellCornerMouseDown, - wtOnCellMouseOver; - - $(this.instance.$table).off('mouseup.' + instance.guid).on('mouseup.' + instance.guid, function (event) { - if (instance.autofill.handle && instance.autofill.handle.isDragged) { - if (instance.autofill.handle.isDragged > 1) { - instance.autofill.apply(); - } - instance.autofill.handle.isDragged = 0; - } - }); - - /* - * Appeding autofill-specific methods to walkontable event settings - */ - wtOnCellCornerMouseDown = this.instance.view.wt.wtSettings.settings.onCellCornerMouseDown; - this.instance.view.wt.wtSettings.settings.onCellCornerMouseDown = function(event) { - instance.autofill.handle.isDragged = 1; - wtOnCellCornerMouseDown(event); - } - - wtOnCellMouseOver = this.instance.view.wt.wtSettings.settings.onCellMouseOver; - this.instance.view.wt.wtSettings.settings.onCellMouseOver = function(event, coords, TD, wt) { - - if (instance.autofill && (!instance.view.isMouseDown() && instance.autofill.handle && instance.autofill.handle.isDragged)) { - instance.autofill.handle.isDragged++; - instance.autofill.showBorder(coords); - instance.autofill.checkIfNewRowNeeded(); - } - - wtOnCellMouseOver(event, coords, TD, wt); - } - - this.instance.view.wt.wtSettings.settings.onCellCornerDblClick = function () { - instance.autofill.selectAdjacent(); - }; - - }; - - /** - * Create fill handle and fill border objects - */ - Autofill.prototype.init = function () { - this.handle = {}; - }, - - /** - * Hide fill handle and fill border permanently - */ - Autofill.prototype.disable = function () { - this.handle.disabled = true; - }, - - /** - * Selects cells down to the last row in the left column, then fills down to that cell - */ - Autofill.prototype.selectAdjacent = function () { - var select, data, r, maxR, c; - - if (this.instance.selection.isMultiple()) { - select = this.instance.view.wt.selections.area.getCorners(); - } - else { - select = this.instance.view.wt.selections.current.getCorners(); - } - - data = this.instance.getData(); - rows : for (r = select[2] + 1; r < this.instance.countRows(); r++) { - for (c = select[1]; c <= select[3]; c++) { - if (data[r][c]) { - break rows; - } - } - if (!!data[r][select[1] - 1] || !!data[r][select[3] + 1]) { - maxR = r; - } - } - if (maxR) { - this.instance.view.wt.selections.fill.clear(); - this.instance.view.wt.selections.fill.add(new WalkontableCellCoords(select[0], select[1])); - this.instance.view.wt.selections.fill.add(new WalkontableCellCoords(maxR, select[3])); - this.apply(); - } - }, - - /** - * Apply fill values to the area in fill border, omitting the selection border - */ - Autofill.prototype.apply = function () { - var drag, select, start, end, _data; - - this.handle.isDragged = 0; - - drag = this.instance.view.wt.selections.fill.getCorners(); - if (!drag) { - return; - } - - this.instance.view.wt.selections.fill.clear(); - - if (this.instance.selection.isMultiple()) { - select = this.instance.view.wt.selections.area.getCorners(); - } - else { - select = this.instance.view.wt.selections.current.getCorners(); - } - - if (drag[0] === select[0] && drag[1] < select[1]) { - start = new WalkontableCellCoords( - drag[0], - drag[1] - ); - end = new WalkontableCellCoords( - drag[2], - select[1] - 1 - ); - } - else if (drag[0] === select[0] && drag[3] > select[3]) { - start = new WalkontableCellCoords( - drag[0], - select[3] + 1 - ); - end = new WalkontableCellCoords( - drag[2], - drag[3] - ); - } - else if (drag[0] < select[0] && drag[1] === select[1]) { - start = new WalkontableCellCoords( - drag[0], - drag[1] - ); - end = new WalkontableCellCoords( - select[0] - 1, - drag[3] - ); - } - else if (drag[2] > select[2] && drag[1] === select[1]) { - start = new WalkontableCellCoords( - select[2] + 1, - drag[1] - ); - end = new WalkontableCellCoords( - drag[2], - drag[3] - ); - } - - if (start) { - var selRange = {from: this.instance.getSelectedRange().from, to: this.instance.getSelectedRange().to}; - - _data = this.instance.getData(selRange.from.row,selRange.from.col,selRange.to.row,selRange.to.col); - - Handsontable.hooks.run(this.instance, 'beforeAutofill', start, end, _data); - - this.instance.populateFromArray(start.row, start.col, _data, end.row, end.col, 'autofill'); - - this.instance.selection.setRangeStart(new WalkontableCellCoords(drag[0], drag[1])); - this.instance.selection.setRangeEnd(new WalkontableCellCoords(drag[2], drag[3])); - } - /*else { - //reset to avoid some range bug - selection.refreshBorders(); - }*/ - }, - - /** - * Show fill border - * @param {WalkontableCellCoords} coords - */ - Autofill.prototype.showBorder = function (coords) { - var topLeft = this.instance.getSelectedRange().getTopLeftCorner(); - var bottomRight = this.instance.getSelectedRange().getBottomRightCorner(); - if (this.instance.getSettings().fillHandle !== 'horizontal' && (bottomRight.row < coords.row || topLeft.row > coords.row)) { - coords = new WalkontableCellCoords(coords.row, bottomRight.col); - } - else if (this.instance.getSettings().fillHandle !== 'vertical') { - coords = new WalkontableCellCoords(bottomRight.row, coords.col); - } - else { - return; //wrong direction - } - - this.instance.view.wt.selections.fill.clear(); - this.instance.view.wt.selections.fill.add(this.instance.getSelectedRange().from); - this.instance.view.wt.selections.fill.add(this.instance.getSelectedRange().to); - this.instance.view.wt.selections.fill.add(coords); - this.instance.view.render(); - } - - Autofill.prototype.checkIfNewRowNeeded = function() { - var fillCorners, - tableRows = this.instance.countRows(), - that = this; - - if(this.instance.view.wt.selections.fill.cellRange && this.addingStarted === false) { - fillCorners = this.instance.view.wt.selections.fill.getCorners(); - - if(fillCorners[2] === tableRows - 1) { - this.addingStarted = true; - - this.instance._registerTimeout(setTimeout(function () { - that.instance.alter('insert_row'); - that.addingStarted = false; - }, 200)); - } - } - - } - - - Handsontable.hooks.add('afterInit', function(){ - var autofill = new Autofill(this); - - if (typeof this.getSettings().fillHandle !== "undefined") { - if (autofill.handle && this.getSettings().fillHandle === false) { - autofill.disable(); - } - else if (!autofill.handle && this.getSettings().fillHandle !== false) { - this.autofill = autofill; - this.autofill.init(); - } - } - - }); - - Handsontable.Autofill = Autofill; - -})(Handsontable); -/** - * Creates an overlay over the original Walkontable instance. The overlay renders the clone of the original Walkontable - * and (optionally) implements behavior needed for native horizontal and vertical scrolling - */ -function WalkontableOverlay() {} - -/* - Possible optimizations: - [x] don't rerender if scroll delta is smaller than the fragment outside of the viewport - [ ] move .style.top change before .draw() - [ ] put .draw() in requestAnimationFrame - [ ] don't rerender rows that remain visible after the scroll - */ - -WalkontableOverlay.prototype.init = function () { - this.TABLE = this.instance.wtTable.TABLE; - this.fixed = this.instance.wtTable.hider; - this.fixedContainer = this.instance.wtTable.holder; - this.scrollHandler = this.getScrollableElement(this.TABLE); - this.$scrollHandler = $(this.scrollHandler); //in future remove jQuery from here -}; - -WalkontableOverlay.prototype.makeClone = function (direction) { - var clone = document.createElement('DIV'); - clone.className = 'ht_clone_' + direction + ' handsontable'; - clone.style.position = 'absolute'; - clone.style.overflow = 'hidden'; - - var table2 = document.createElement('TABLE'); - table2.className = this.instance.wtTable.TABLE.className; - clone.appendChild(table2); - - this.instance.wtTable.holder.parentNode.appendChild(clone); - - return new Walkontable({ - cloneSource: this.instance, - cloneOverlay: this, - table: table2 - }); -}; - -WalkontableOverlay.prototype.getScrollableElement = function (TABLE) { - var el = TABLE.parentNode; - while (el && el.style) { - if (el.style.overflow !== 'visible' && el.style.overflow !== '') { - return el; - } - if (this instanceof WalkontableHorizontalScrollbarNative && el.style.overflowX !== 'visible' && el.style.overflowX !== '') { - return el; - } - el = el.parentNode; - } - return window; -}; - -WalkontableOverlay.prototype.onScroll = function () { - - this.windowScrollPosition = this.getScrollPosition(); - this.readSettings(); //read window scroll position - - this.resetFixedPosition(); //may be redundant -}; - -WalkontableOverlay.prototype.availableSize = function () { - var availableSize; - - if (this.windowScrollPosition > this.tableParentOffset /*&& last > -1*/) { //last -1 means that viewport is scrolled behind the table - if (this.instance.wtTable.getLastVisibleRow() === this.total - 1) { - availableSize = Handsontable.Dom.outerHeight(this.TABLE); - } - else { - availableSize = this.windowSize; - } - } - else { - availableSize = this.windowSize - (this.tableParentOffset); - } - - return availableSize; -}; - -WalkontableOverlay.prototype.refresh = function (selectionsOnly) { - this.clone && this.clone.draw(selectionsOnly); -}; - -WalkontableOverlay.prototype.destroy = function () { - this.$scrollHandler.off('.' + this.clone.guid); - $(window).off('.' + this.clone.guid); - $(document).off('.' + this.clone.guid); - $(document.body).off('.' + this.clone.guid); -}; -/** - * WalkontableAbstractStrategy (WalkontableColumnStrategy and WalkontableRowStrategy inherit from this) - * @constructor - */ -function WalkontableAbstractStrategy(instance) { - this.instance = instance; -} - -WalkontableAbstractStrategy.prototype.getSize = function (index) { - return this.cellSizes[index]; -}; - -WalkontableAbstractStrategy.prototype.getContainerSize = function (proposedSize) { - return typeof this.containerSizeFn === 'function' ? this.containerSizeFn(proposedSize) : this.containerSizeFn; -}; - -WalkontableAbstractStrategy.prototype.countVisible = function () { - return this.cellCount; -}; - -WalkontableAbstractStrategy.prototype.isLastIncomplete = function () { - return this.remainingSize > 0; -}; -function WalkontableBorder(instance, settings) { - var style; - - if(!settings){ - return; - } - - //reference to instance - this.instance = instance; - this.settings = settings; - - this.main = document.createElement("div"); - style = this.main.style; - style.position = 'absolute'; - style.top = 0; - style.left = 0; - - var borderDivs = ['top','left','bottom','right','corner']; - - for (var i = 0; i < 5; i++) { - var position = borderDivs[i]; - - var DIV = document.createElement('DIV'); - DIV.className = 'wtBorder ' + (this.settings.className || ''); // + borderDivs[i]; - if(this.settings[position] && this.settings[position].hide){ - DIV.className += " hidden"; - } - - style = DIV.style; - style.backgroundColor = (this.settings[position] && this.settings[position].color) ? this.settings[position].color : settings.border.color; - style.height = (this.settings[position] && this.settings[position].width) ? this.settings[position].width + 'px' : settings.border.width + 'px'; - style.width = (this.settings[position] && this.settings[position].width) ? this.settings[position].width + 'px' : settings.border.width + 'px'; - - this.main.appendChild(DIV); - } - - this.top = this.main.childNodes[0]; - this.left = this.main.childNodes[1]; - this.bottom = this.main.childNodes[2]; - this.right = this.main.childNodes[3]; - - this.topStyle = this.top.style; - this.leftStyle = this.left.style; - this.bottomStyle = this.bottom.style; - this.rightStyle = this.right.style; - - this.corner = this.main.childNodes[4]; - this.corner.className += ' corner'; - this.cornerStyle = this.corner.style; - this.cornerStyle.width = '5px'; - this.cornerStyle.height = '5px'; - this.cornerStyle.border = '2px solid #FFF'; - - this.disappear(); - if (!instance.wtTable.bordersHolder) { - instance.wtTable.bordersHolder = document.createElement('div'); - instance.wtTable.bordersHolder.className = 'htBorders'; - instance.wtTable.hider.appendChild(instance.wtTable.bordersHolder); - - } - instance.wtTable.bordersHolder.insertBefore(this.main, instance.wtTable.bordersHolder.firstChild); - - var down = false; - var $body = $(document.body); - - $body.on('mousedown.walkontable.' + instance.guid, function () { - down = true; - }); - - $body.on('mouseup.walkontable.' + instance.guid, function () { - down = false - }); - - $(this.main.childNodes).on('mouseenter', function (event) { - if (!down || !instance.getSetting('hideBorderOnMouseDownOver')) { - return; - } - event.preventDefault(); - event.stopImmediatePropagation(); - - var bounds = this.getBoundingClientRect(); - - var $this = $(this); - $this.hide(); - - var isOutside = function (event) { - if (event.clientY < Math.floor(bounds.top)) { - return true; - } - if (event.clientY > Math.ceil(bounds.top + bounds.height)) { - return true; - } - if (event.clientX < Math.floor(bounds.left)) { - return true; - } - if (event.clientX > Math.ceil(bounds.left + bounds.width)) { - return true; - } - }; - - $body.on('mousemove.border.' + instance.guid, function (event) { - if (isOutside(event)) { - $body.off('mousemove.border.' + instance.guid); - $this.show(); - } - }); - }); -} - -/** - * Show border around one or many cells - * @param {Array} corners - */ -WalkontableBorder.prototype.appear = function (corners) { - var isMultiple, fromTD, toTD, fromOffset, toOffset, containerOffset, top, minTop, left, minLeft, height, width; - if (this.disabled) { - return; - } - - var instance = this.instance; - - var fromRow - , fromColumn - , toRow - , toColumn - , i - , ilen - , s; - - if (instance.cloneOverlay instanceof WalkontableVerticalScrollbarNative || instance.cloneOverlay instanceof WalkontableCornerScrollbarNative) { - ilen = instance.getSetting('fixedRowsTop'); - } - else { - ilen = instance.wtTable.getRowStrategy().countVisible(); - } - - for (i = 0; i < ilen; i++) { - s = instance.wtTable.rowFilter.visibleToSource(i); - if (s >= corners[0] && s <= corners[2]) { - fromRow = s; - break; - } - } - - for (i = ilen - 1; i >= 0; i--) { - s = instance.wtTable.rowFilter.visibleToSource(i); - if (s >= corners[0] && s <= corners[2]) { - toRow = s; - break; - } - } - - if (instance.cloneOverlay instanceof WalkontableHorizontalScrollbarNative || instance.cloneOverlay instanceof WalkontableCornerScrollbarNative) { - ilen = instance.getSetting('fixedColumnsLeft'); - } - else { - ilen = instance.wtTable.getColumnStrategy().cellCount; - } - - for (i = 0; i < ilen; i++) { - s = instance.wtTable.columnFilter.visibleToSource(i); - if (s >= corners[1] && s <= corners[3]) { - fromColumn = s; - break; - } - } - - for (i = ilen - 1; i >= 0; i--) { - s = instance.wtTable.columnFilter.visibleToSource(i); - if (s >= corners[1] && s <= corners[3]) { - toColumn = s; - break; - } - } - - if (fromRow !== void 0 && fromColumn !== void 0) { - isMultiple = (fromRow !== toRow || fromColumn !== toColumn); - fromTD = instance.wtTable.getCell(new WalkontableCellCoords(fromRow, fromColumn)); - toTD = isMultiple ? instance.wtTable.getCell(new WalkontableCellCoords(toRow, toColumn)) : fromTD; - fromOffset = Handsontable.Dom.offset(fromTD); - toOffset = isMultiple ? Handsontable.Dom.offset(toTD) : fromOffset; - containerOffset = Handsontable.Dom.offset(instance.wtTable.TABLE); - - minTop = fromOffset.top; - height = toOffset.top + Handsontable.Dom.outerHeight(toTD) - minTop; - minLeft = fromOffset.left; - width = toOffset.left + Handsontable.Dom.outerWidth(toTD) - minLeft; - - top = minTop - containerOffset.top - 1; - left = minLeft - containerOffset.left - 1; - - var style = Handsontable.Dom.getComputedStyle(fromTD); - if (parseInt(style['borderTopWidth'], 10) > 0) { - top += 1; - height = height > 0 ? height - 1 : 0; - } - if (parseInt(style['borderLeftWidth'], 10) > 0) { - left += 1; - width = width > 0 ? width - 1 : 0; - } - } - else { - this.disappear(); - return; - } - - this.topStyle.top = top + 'px'; - this.topStyle.left = left + 'px'; - this.topStyle.width = width + 'px'; - this.topStyle.display = 'block'; - - - - this.leftStyle.top = top + 'px'; - this.leftStyle.left = left + 'px'; - this.leftStyle.height = height + 'px'; - this.leftStyle.display = 'block'; - - var delta = Math.floor(this.settings.border.width / 2); - - this.bottomStyle.top = top + height - delta + 'px'; - this.bottomStyle.left = left + 'px'; - this.bottomStyle.width = width + 'px'; - this.bottomStyle.display = 'block'; - - this.rightStyle.top = top + 'px'; - this.rightStyle.left = left + width - delta + 'px'; - this.rightStyle.height = height + 1 + 'px'; - this.rightStyle.display = 'block'; - - if (!this.hasSetting(this.settings.border.cornerVisible)) { - this.cornerStyle.display = 'none'; - } - else { - this.cornerStyle.top = top + height - 4 + 'px'; - this.cornerStyle.left = left + width - 4 + 'px'; - this.cornerStyle.display = 'block'; - } -}; - -/** - * Hide border - */ -WalkontableBorder.prototype.disappear = function () { - this.topStyle.display = 'none'; - this.leftStyle.display = 'none'; - this.bottomStyle.display = 'none'; - this.rightStyle.display = 'none'; - this.cornerStyle.display = 'none'; -}; - -WalkontableBorder.prototype.hasSetting = function (setting) { - if (typeof setting === 'function') { - return setting(); - } - return !!setting; -}; - -/** - * WalkontableCellCoords holds cell coordinates (row, column) and few metiod to validate them and retrieve as an array or an object - * TODO: change interface to WalkontableCellCoords(row, col) everywhere, remove those unnecessary setter and getter functions - */ - -function WalkontableCellCoords(row, col) { - if (typeof row !== 'undefined' && typeof col !== 'undefined') { - this.row = row; - this.col = col; - } - else { - this.row = null; - this.col = null; - } -} - -/** - * Returns boolean information if given set of coordinates is valid in context of a given Walkontable instance - * @param instance - * @returns {boolean} - */ -WalkontableCellCoords.prototype.isValid = function (instance) { - //is it a valid cell index (0 or higher) - if (this.row < 0 || this.col < 0) { - return false; - } - - //is selection within total rows and columns - if (this.row >= instance.getSetting('totalRows') || this.col >= instance.getSetting('totalColumns')) { - return false; - } - - return true; -}; - -/** - * Returns boolean information if this cell coords are the same as cell coords given as a parameter - * @param {WalkontableCellCoords} cellCoords - * @returns {boolean} - */ -WalkontableCellCoords.prototype.isEqual = function (cellCoords) { - if (cellCoords === this) { - return true; - } - return (this.row === cellCoords.row && this.col === cellCoords.col); -}; - -WalkontableCellCoords.prototype.isSouthEastOf = function (testedCoords) { - return this.row >= testedCoords.row && this.col >= testedCoords.col; -}; - -WalkontableCellCoords.prototype.isNorthWestOf = function (testedCoords) { - return this.row <= testedCoords.row && this.col <= testedCoords.col; -}; - -window.WalkontableCellCoords = WalkontableCellCoords; //export -/** - * A cell range is a set of exactly two WalkontableCellCoords (that can be the same or different) - */ - -function WalkontableCellRange(highlight, from, to) { - this.highlight = highlight; //this property is used to draw bold border around a cell where selection was started and to edit the cell when you press Enter - this.from = from; //this property is usually the same as highlight, but in Excel there is distinction - one can change highlight within a selection - this.to = to; -} - -WalkontableCellRange.prototype.isValid = function (instance) { - return (this.from.isValid(instance) && this.to.isValid(instance)); -}; - -WalkontableCellRange.prototype.isSingle = function () { - return (this.from.row === this.to.row && this.from.col === this.to.col); -}; - -/** - * Returns boolean information if given cell coords is within `from` and `to` cell coords of this range - * @param {WalkontableCellCoords} cellCoords - * @returns {boolean} - */ -WalkontableCellRange.prototype.includes = function (cellCoords) { - var topLeft = this.getTopLeftCorner(); - var bottomRight = this.getBottomRightCorner(); - return (topLeft.row <= cellCoords.row && bottomRight.row >= cellCoords.row && topLeft.col <= cellCoords.col && bottomRight.col >= cellCoords.col); -}; - -WalkontableCellRange.prototype.includesRange = function (testedRange) { - return this.includes(testedRange.getTopLeftCorner()) && this.includes(testedRange.getBottomRightCorner()); -}; - -/** - * Returns true if tested range overlaps with the range. - * Range A is considered to to be overlapping with range B if intersection of A and B or B and A is not empty. - * @param testedRange - * @returns {boolean} - */ -WalkontableCellRange.prototype.overlaps = function (testedRange) { - return testedRange.isSouthEastOf(this.getTopLeftCorner()) && testedRange.isNorthWestOf(this.getBottomRightCorner()); -}; - -WalkontableCellRange.prototype.isSouthEastOf = function (testedCoords) { - return this.getTopLeftCorner().isSouthEastOf(testedCoords) || this.getBottomRightCorner().isSouthEastOf(testedCoords); -}; - -WalkontableCellRange.prototype.isNorthWestOf = function (testedCoords) { - return this.getTopLeftCorner().isNorthWestOf(testedCoords) || this.getBottomRightCorner().isNorthWestOf(testedCoords); -}; - -/** - * Adds a cell to a range (only if exceeds corners of the range). Returns information if range was expanded - * @param {WalkontableCellCoords} cellCoords - * @returns {boolean} - */ -WalkontableCellRange.prototype.expand = function (cellCoords) { - var topLeft = this.getTopLeftCorner(); - var bottomRight = this.getBottomRightCorner(); - if (cellCoords.row < topLeft.row || cellCoords.col < topLeft.col || cellCoords.row > bottomRight.row || cellCoords.col > bottomRight.col) { - this.from = new WalkontableCellCoords(Math.min(topLeft.row, cellCoords.row), Math.min(topLeft.col, cellCoords.col)); - this.to = new WalkontableCellCoords(Math.max(bottomRight.row, cellCoords.row), Math.max(bottomRight.col, cellCoords.col)); - return true; - } - return false; -}; - -WalkontableCellRange.prototype.expandByRange = function (expandingRange) { - if (this.includesRange(expandingRange) || !this.overlaps(expandingRange)){ - return false; - } - - var topLeft = this.getTopLeftCorner(); - var bottomRight = this.getBottomRightCorner(); - - var expandingTopLeft = expandingRange.getTopLeftCorner(); - var expandingBottomRight = expandingRange.getBottomRightCorner(); - - var resultTopRow = Math.min(topLeft.row, expandingTopLeft.row); - var resultTopCol = Math.min(topLeft.col, expandingTopLeft.col); - var resultBottomRow = Math.max(bottomRight.row, expandingBottomRight.row); - var resultBottomCol = Math.max(bottomRight.col, expandingBottomRight.col); - - this.from = new WalkontableCellCoords(resultTopRow, resultTopCol); - this.to = new WalkontableCellCoords(resultBottomRow, resultBottomCol); - - return true; - - -}; - -WalkontableCellRange.prototype.getTopLeftCorner = function () { - return new WalkontableCellCoords(Math.min(this.from.row, this.to.row), Math.min(this.from.col, this.to.col)); -}; - -WalkontableCellRange.prototype.getBottomRightCorner = function () { - return new WalkontableCellCoords(Math.max(this.from.row, this.to.row), Math.max(this.from.col, this.to.col)); -}; - -WalkontableCellRange.prototype.getInner = function () { - var topLeft = this.getTopLeftCorner(); - var bottomRight = this.getBottomRightCorner(); - var out = []; - for (var r = topLeft.row; r <= bottomRight.row; r++) { - for (var c = topLeft.col; c <= bottomRight.col; c++) { - if (!(this.from.row === r && this.from.col === c) && !(this.to.row === r && this.to.col === c)) { - out.push(new WalkontableCellCoords(r, c)); - } - } - } - return out; -}; - -WalkontableCellRange.prototype.getAll = function () { - var topLeft = this.getTopLeftCorner(); - var bottomRight = this.getBottomRightCorner(); - var out = []; - for (var r = topLeft.row; r <= bottomRight.row; r++) { - for (var c = topLeft.col; c <= bottomRight.col; c++) { - if (topLeft.row === r && topLeft.col === c) { - out.push(topLeft); - } - else if (bottomRight.row === r && bottomRight.col === c) { - out.push(bottomRight); - } - else { - out.push(new WalkontableCellCoords(r, c)); - } - } - } - return out; -}; - -/** - * Runs a callback function against all cells in the range. You can break the iteration by returning false in the callback function - * @param callback {Function} - */ -WalkontableCellRange.prototype.forAll = function (callback) { - var topLeft = this.getTopLeftCorner(); - var bottomRight = this.getBottomRightCorner(); - for (var r = topLeft.row; r <= bottomRight.row; r++) { - for (var c = topLeft.col; c <= bottomRight.col; c++) { - var breakIteration = callback(r, c); - if (breakIteration === false) { - return; - } - } - } -}; - -window.WalkontableCellRange = WalkontableCellRange; //export -/** - * WalkontableClassNameList - * @constructor - */ -function WalkontableClassNameCache() { - this.cache = []; -} - -WalkontableClassNameCache.prototype.add = function (r, c, cls) { - if (!this.cache[r]) { - this.cache[r] = []; - } - if (!this.cache[r][c]) { - this.cache[r][c] = []; - } - this.cache[r][c][cls] = true; -}; - -WalkontableClassNameCache.prototype.test = function (r, c, cls) { - return (this.cache[r] && this.cache[r][c] && this.cache[r][c][cls]); -}; -/** - * WalkontableColumnFilter - * @constructor - */ -function WalkontableColumnFilter(total, countTH) { - this.total = total; - this.countTH = countTH; -} - -WalkontableColumnFilter.prototype.visibleToSource = function (n) { - return n; -}; - -WalkontableColumnFilter.prototype.sourceToVisible = function (n) { - return n; -}; - -WalkontableColumnFilter.prototype.offsettedTH = function (n) { - return n - this.countTH; -}; - -WalkontableColumnFilter.prototype.unOffsettedTH = function (n) { - return n + this.countTH; -}; - -WalkontableColumnFilter.prototype.visibleRowHeadedColumnToSourceColumn = function (n) { - return this.visibleToSource(this.offsettedTH(n)); -}; - -WalkontableColumnFilter.prototype.sourceColumnToVisibleRowHeadedColumn = function (n) { - return this.unOffsettedTH(this.sourceToVisible(n)); -}; -/** - * WalkontableColumnStrategy - * @param containerSizeFn - * @param sizeAtIndex - * @param strategy - all, last, none - * @constructor - */ -function WalkontableColumnStrategy(instance, containerSizeFn, sizeAtIndex, strategy) { - var size - , i = 0; - - WalkontableAbstractStrategy.apply(this, arguments); - - this.containerSizeFn = containerSizeFn; - this.cellSizesSum = 0; - this.cellSizes = []; - this.cellStretch = []; - this.cellCount = 0; - this.visibleCellCount = 0; - this.remainingSize = 0; - this.strategy = strategy; - - //step 1 - determine cells that fit containerSize and cache their widths - while (true) { - size = sizeAtIndex(i); - if (size === void 0) { - break; //total columns exceeded - } - if (this.cellSizesSum < this.getContainerSize(this.cellSizesSum + size)) { - this.visibleCellCount++; - } - this.cellSizes.push(size); - this.cellSizesSum += size; - this.cellCount++; - - i++; - } - - var containerSize = this.getContainerSize(this.cellSizesSum); - this.remainingSize = this.cellSizesSum - containerSize; - //negative value means the last cell is fully visible and there is some space left for stretching - //positive value means the last cell is not fully visible -} - -WalkontableColumnStrategy.prototype = new WalkontableAbstractStrategy(); - -WalkontableColumnStrategy.prototype.getSize = function (index) { - return this.cellSizes[index] + (this.cellStretch[index] || 0); -}; - -WalkontableColumnStrategy.prototype.stretch = function () { - //step 2 - apply stretching strategy - var containerSize - , i = 0; - - containerSize = this.instance.wtTable.allRowsInViewport() ? this.getContainerSize() : this.getContainerSize(Infinity); - - this.remainingSize = this.cellSizesSum - containerSize; - - this.cellStretch.length = 0; //clear previous stretch - - if (this.strategy === 'all') { - if (this.remainingSize < 0) { - var ratio = containerSize / this.cellSizesSum; - var newSize; - - while (i < this.cellCount - 1) { //"i < this.cellCount - 1" is needed because last cellSize is adjusted after the loop - newSize = Math.floor(ratio * this.cellSizes[i]); - this.remainingSize += newSize - this.cellSizes[i]; - this.cellStretch[i] = newSize - this.cellSizes[i]; - i++; - } - this.cellStretch[this.cellCount - 1] = -this.remainingSize; - this.remainingSize = 0; - } - } - else if (this.strategy === 'last') { - if (this.remainingSize < 0 && containerSize !== Infinity) { //Infinity is with native scroll when the table is wider than the viewport (TODO: test) - this.cellStretch[this.cellCount - 1] = -this.remainingSize; - this.remainingSize = 0; - } - } -}; - -WalkontableColumnStrategy.prototype.countVisible = function () { - return this.visibleCellCount; -}; - -WalkontableColumnStrategy.prototype.isLastIncomplete = function () { - - var firstRow = this.instance.wtTable.getFirstVisibleRow(); - var lastCol = this.instance.wtTable.getLastVisibleColumn(); - var cell = this.instance.wtTable.getCell(new WalkontableCellCoords(firstRow, lastCol)); - var cellOffset = Handsontable.Dom.offset(cell); - var cellWidth = Handsontable.Dom.outerWidth(cell); - var cellEnd = cellOffset.left + cellWidth; - - var viewportOffsetLeft = this.instance.wtScrollbars.vertical.getScrollPosition(); - var viewportWitdh = this.instance.wtViewport.getViewportWidth(); - var viewportEnd = viewportOffsetLeft + viewportWitdh; - - - return viewportEnd >= cellEnd; -}; - -function Walkontable(settings) { - var originalHeaders = []; - - this.guid = 'wt_' + walkontableRandomString(); //this is the namespace for global events - - //bootstrap from settings - if (settings.cloneSource) { - this.cloneSource = settings.cloneSource; - this.cloneOverlay = settings.cloneOverlay; - this.wtSettings = settings.cloneSource.wtSettings; - this.wtTable = new WalkontableTable(this, settings.table); - this.wtScroll = new WalkontableScroll(this); - this.wtViewport = settings.cloneSource.wtViewport; - this.wtEvent = new WalkontableEvent(this); - this.selections = this.generateSelectionClones(this.cloneSource.selections); - } - else { - this.wtSettings = new WalkontableSettings(this, settings); - this.wtTable = new WalkontableTable(this, settings.table); - this.wtScroll = new WalkontableScroll(this); - this.wtViewport = new WalkontableViewport(this); - this.wtEvent = new WalkontableEvent(this); - var selectionSettings = this.getSetting('selections'); - this.selections = selectionSettings ? this.generateSelections(selectionSettings) : []; - - this.wtScrollbars = new WalkontableScrollbars(this); - } - - //find original headers - if (this.wtTable.THEAD.childNodes.length && this.wtTable.THEAD.childNodes[0].childNodes.length) { - for (var c = 0, clen = this.wtTable.THEAD.childNodes[0].childNodes.length; c < clen; c++) { - originalHeaders.push(this.wtTable.THEAD.childNodes[0].childNodes[c].innerHTML); - } - if (!this.getSetting('columnHeaders').length) { - this.update('columnHeaders', [function (column, TH) { - Handsontable.Dom.fastInnerText(TH, originalHeaders[column]); - }]); - } - } - - - - this.drawn = false; - this.drawInterrupted = false; -} - -Walkontable.prototype.draw = function (selectionsOnly) { - this.drawInterrupted = false; - if (!selectionsOnly && !Handsontable.Dom.isVisible(this.wtTable.TABLE)) { - this.drawInterrupted = true; //draw interrupted because TABLE is not visible - return; - } - - selectionsOnly = selectionsOnly && this.getSetting('offsetRow') === this.lastOffsetRow; - this.lastOffsetRow = this.getSetting('offsetRow'); - - var totalRows = this.getSetting('totalRows'); - - if (this.lastOffsetRow > totalRows && totalRows > 0) { - this.scrollVertical(-Infinity); //TODO: probably very inefficient! - this.scrollViewport(new WalkontableCellCoords(totalRows - 1, 0)); - } - - - this.wtTable.draw(selectionsOnly); - return this; -}; - -Walkontable.prototype.update = function (settings, value) { - return this.wtSettings.update(settings, value); -}; - -Walkontable.prototype.scrollVertical = function (delta) { - var result = this.wtScroll.scrollVertical(delta); - - this.getSetting('onScrollVertically'); - - return result; -}; - -Walkontable.prototype.scrollHorizontal = function (delta) { - var result = this.wtScroll.scrollHorizontal(delta); - - this.getSetting('onScrollHorizontally'); - - return result; -}; - -/** - * Scrolls the viewport to a cell (rerenders if needed) - * @param {WalkontableCellCoords} coords - * @returns {Walkontable} - */ - -Walkontable.prototype.scrollViewport = function (coords) { - this.wtScroll.scrollViewport(coords); - return this; -}; - -Walkontable.prototype.getViewport = function () { - return [ - this.wtTable.getFirstVisibleRow(), - this.wtTable.getFirstVisibleColumn(), - this.wtTable.getLastVisibleRow(), - this.wtTable.getLastVisibleColumn() - ]; -}; - -Walkontable.prototype.getSetting = function (key, param1, param2, param3, param4) { - return this.wtSettings.getSetting(key, param1, param2, param3, param4); //this is faster than .apply - https://github.com/handsontable/jquery-handsontable/wiki/JavaScript-&-DOM-performance-tips -}; - -Walkontable.prototype.hasSetting = function (key) { - return this.wtSettings.has(key); -}; - -Walkontable.prototype.generateSelections = function (settings) { - var selections = []; - for (var i = 0, ilen = settings.length; i < ilen; i++) { - var sel = new WalkontableSelection(this, settings[i]); - selections.push(sel); - if (sel.settings.className) { - selections[sel.settings.className] = sel; //create shorthand access - } - } - return selections; -}; - -Walkontable.prototype.generateSelectionClones = function (selections) { - var clones = []; - for (var i = 0, ilen = selections.length; i < ilen; i++) { - var sel = selections[i].makeClone(this); - clones.push(sel); - if (sel.settings.className) { - clones[sel.settings.className] = sel; //create shorthand access - } - } - return clones; -}; - -Walkontable.prototype.destroy = function () { - $(window).off('.' + this.guid); - $(document.body).off('.' + this.guid); - this.wtScrollbars.destroy(); - this.wtEvent && this.wtEvent.destroy(); -}; -/** - * A overlay that renders ALL available rows & columns positioned on top of the original Walkontable instance and all other overlays. - * Used for debugging purposes to see if the other overlays (that render only part of the rows & columns) are positioned correctly - * @param instance - * @constructor - */ -function WalkontableDebugOverlay(instance) { - this.instance = instance; - this.init(); - this.clone = this.makeClone('debug'); - this.clone.wtTable.holder.style.opacity = 0.4; - this.clone.wtTable.holder.style.textShadow = '0 0 2px #ff0000'; - this.lastTimeout = null; - - var that = this; - var lastX = 0; - var lastY = 0; - var overlayContainer = that.clone.wtTable.holder.parentNode; - - $(document.body).on('mousemove.' + this.instance.guid, function (event) { - if (!that.instance.wtTable.holder.parentNode) { - return; //removed from DOM - } - if ((event.clientX - lastX > -5 && event.clientX - lastX < 5) && (event.clientY - lastY > -5 && event.clientY - lastY < 5)) { - return; //ignore minor mouse movement - } - lastX = event.clientX; - lastY = event.clientY; - Handsontable.Dom.addClass(overlayContainer, 'wtDebugHidden'); - Handsontable.Dom.removeClass(overlayContainer, 'wtDebugVisible'); - clearTimeout(this.lastTimeout); - this.lastTimeout = setTimeout(function () { - Handsontable.Dom.removeClass(overlayContainer, 'wtDebugHidden'); - Handsontable.Dom.addClass(overlayContainer, 'wtDebugVisible'); - }, 1000); - }); -} - -WalkontableDebugOverlay.prototype = new WalkontableOverlay(); - -WalkontableDebugOverlay.prototype.resetFixedPosition = function () { - if (!this.instance.wtTable.holder.parentNode) { - return; //removed from DOM - } - var elem = this.clone.wtTable.holder.parentNode; - var box = this.instance.wtTable.holder.getBoundingClientRect(); - elem.style.top = Math.ceil(box.top, 10) + 'px'; - elem.style.left = Math.ceil(box.left, 10) + 'px'; -}; - -WalkontableDebugOverlay.prototype.getScrollPosition = function () { -}; - -WalkontableDebugOverlay.prototype.getLastCell = function () { -}; - -WalkontableDebugOverlay.prototype.applyToDOM = function () { -}; - -WalkontableDebugOverlay.prototype.scrollTo = function () { -}; - -WalkontableDebugOverlay.prototype.readWindowSize = function () { -}; - -WalkontableDebugOverlay.prototype.readSettings = function () { -}; - -WalkontableDebugOverlay.prototype.destroy = function () { - WalkontableOverlay.prototype.destroy.call(this); - clearTimeout(this.lastTimeout); -}; -function WalkontableEvent(instance) { - var that = this; - - //reference to instance - this.instance = instance; - - var dblClickOrigin = [null, null]; - this.dblClickTimeout = [null, null]; - - var onMouseDown = function (event) { - var cell = that.parentCell(event.target); - if (Handsontable.Dom.hasClass(event.target, 'corner')) { - that.instance.getSetting('onCellCornerMouseDown', event, event.target); - } - else if (cell.TD) { - if (that.instance.hasSetting('onCellMouseDown')) { - that.instance.getSetting('onCellMouseDown', event, cell.coords, cell.TD, that.instance); - } - } - - if (event.button !== 2) { //if not right mouse button - if (cell.TD) { - dblClickOrigin[0] = cell.TD; - clearTimeout(that.dblClickTimeout[0]); - that.dblClickTimeout[0] = setTimeout(function () { - dblClickOrigin[0] = null; - }, 1000); - } - } - }; - - var lastMouseOver; - var onMouseOver = function (event) { - if (that.instance.hasSetting('onCellMouseOver')) { - var TABLE = that.instance.wtTable.TABLE; - var TD = Handsontable.Dom.closest(event.target, ['TD', 'TH'], TABLE); - if (TD && TD !== lastMouseOver && Handsontable.Dom.isChildOf(TD, TABLE)) { - lastMouseOver = TD; - that.instance.getSetting('onCellMouseOver', event, that.instance.wtTable.getCoords(TD), TD, that.instance); - } - } - }; - -/* var lastMouseOut; - var onMouseOut = function (event) { - if (that.instance.hasSetting('onCellMouseOut')) { - var TABLE = that.instance.wtTable.TABLE; - var TD = Handsontable.Dom.closest(event.target, ['TD', 'TH'], TABLE); - if (TD && TD !== lastMouseOut && Handsontable.Dom.isChildOf(TD, TABLE)) { - lastMouseOut = TD; - if (TD.nodeName === 'TD') { - that.instance.getSetting('onCellMouseOut', event, that.instance.wtTable.getCoords(TD), TD); - } - } - } - };*/ - - var onMouseUp = function (event) { - if (event.button !== 2) { //if not right mouse button - var cell = that.parentCell(event.target); - - if (cell.TD === dblClickOrigin[0] && cell.TD === dblClickOrigin[1]) { - if (Handsontable.Dom.hasClass(event.target, 'corner')) { - that.instance.getSetting('onCellCornerDblClick', event, cell.coords, cell.TD, that.instance); - } - else { - that.instance.getSetting('onCellDblClick', event, cell.coords, cell.TD, that.instance); - } - - dblClickOrigin[0] = null; - dblClickOrigin[1] = null; - } - else if (cell.TD === dblClickOrigin[0]) { - dblClickOrigin[1] = cell.TD; - clearTimeout(that.dblClickTimeout[1]); - that.dblClickTimeout[1] = setTimeout(function () { - dblClickOrigin[1] = null; - }, 500); - } - } - }; - - $(this.instance.wtTable.holder).on('mousedown', onMouseDown); - $(this.instance.wtTable.TABLE).on('mouseover', onMouseOver); - $(this.instance.wtTable.holder).on('mouseup', onMouseUp); - - $(window).on('resize.' + this.instance.guid, function () { - that.instance.draw(); - }); -} - -WalkontableEvent.prototype.parentCell = function (elem) { - var cell = {}; - var TABLE = this.instance.wtTable.TABLE; - var TD = Handsontable.Dom.closest(elem, ['TD', 'TH'], TABLE); - - if (TD && Handsontable.Dom.isChildOf(TD, TABLE)) { - cell.coords = this.instance.wtTable.getCoords(TD); - cell.TD = TD; - } - else if (Handsontable.Dom.hasClass(elem, 'wtBorder') && Handsontable.Dom.hasClass(elem, 'current')) { - cell.coords = this.instance.selections.current.cellRange.highlight; - cell.TD = this.instance.wtTable.getCell(cell.coords); - } - return cell; -}; - -WalkontableEvent.prototype.destroy = function () { - clearTimeout(this.dblClickTimeout[0]); - clearTimeout(this.dblClickTimeout[1]); -}; -function walkontableRangesIntersect() { - var from = arguments[0]; - var to = arguments[1]; - for (var i = 1, ilen = arguments.length / 2; i < ilen; i++) { - if (from <= arguments[2 * i + 1] && to >= arguments[2 * i]) { - return true; - } - } - return false; -} - -/** - * Generates a random hex string. Used as namespace for Walkontable instance events. - * @return {String} - 16 character random string: "92b1bfc74ec4" - */ -function walkontableRandomString() { - function s4() { - return Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1); - } - - return s4() + s4() + s4() + s4(); -} -/** - * http://notes.jetienne.com/2011/05/18/cancelRequestAnimFrame-for-paul-irish-requestAnimFrame.html - */ -window.requestAnimFrame = (function () { - return window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame || - function (/* function */ callback, /* DOMElement */ element) { - return window.setTimeout(callback, 1000 / 60); - }; -})(); - -window.cancelRequestAnimFrame = (function () { - return window.cancelAnimationFrame || - window.webkitCancelRequestAnimationFrame || - window.mozCancelRequestAnimationFrame || - window.oCancelRequestAnimationFrame || - window.msCancelRequestAnimationFrame || - clearTimeout -})(); - -//http://snipplr.com/view/13523/ -//modified for speed -//http://jsperf.com/getcomputedstyle-vs-style-vs-css/8 -if (!window.getComputedStyle) { - (function () { - var elem; - - var styleObj = { - getPropertyValue: function getPropertyValue(prop) { - if (prop == 'float') prop = 'styleFloat'; - return elem.currentStyle[prop.toUpperCase()] || null; - } - }; - - window.getComputedStyle = function (el) { - elem = el; - return styleObj; - } - })(); -} - -/** - * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim - */ -if (!String.prototype.trim) { - var trimRegex = /^\s+|\s+$/g; - String.prototype.trim = function () { - return this.replace(trimRegex, ''); - }; -} -/** - * WalkontableRowFilter - * @constructor - */ -function WalkontableRowFilter(offset, total, countTH) { - this.offset = offset; - this.total = total; - this.countTH = countTH; -} - -WalkontableRowFilter.prototype.offsetted = function (n) { - return n + this.offset; -}; - -WalkontableRowFilter.prototype.unOffsetted = function (n) { - return n - this.offset; -}; - -WalkontableRowFilter.prototype.visibleToSource = function (n) { - return this.offsetted(n); -}; - -WalkontableRowFilter.prototype.sourceToVisible = function (n) { - return this.unOffsetted(n); -}; - -WalkontableRowFilter.prototype.offsettedTH = function (n) { - return n - this.countTH; -}; - -WalkontableRowFilter.prototype.visibleColHeadedRowToSourceRow = function (n) { - return this.visibleToSource(this.offsettedTH(n)); -}; - -WalkontableRowFilter.prototype.sourceRowToVisibleColHeadedRow = function (n) { - return this.unOffsettedTH(this.sourceToVisible(n)); -}; - -/** - * WalkontableRowStrategy - * @param containerSizeFn - * @param sizeAtIndex - * @constructor - */ -function WalkontableRowStrategy(instance, containerSizeFn, sizeAtIndex) { - - WalkontableAbstractStrategy.apply(this, arguments); - - this.containerSizeFn = containerSizeFn; - this.sizeAtIndex = sizeAtIndex; - this.cellSizesSum = 0; - this.cellSizes = []; - this.cellCount = 0; - this.visiblCellCount = 0; - this.remainingSize = -Infinity; - this.maxOuts = 10; //max outs in one direction (before and after table) - this.curOuts = this.maxOuts; -} - -WalkontableRowStrategy.prototype = new WalkontableAbstractStrategy(); - -WalkontableRowStrategy.prototype.add = function (i, TD) { - if(!this.canRenderMoreRows()){ - return false; - } - - var size = this.sizeAtIndex(i, TD); - - if (size === void 0) { - return false; //total rows exceeded - } - - var containerSize = this.getContainerSize(this.cellSizesSum + size); - this.cellSizes.push(size); - this.cellSizesSum += size; - - this.cellCount++; - this.remainingSize = this.cellSizesSum - containerSize; - - if (this.remainingSize <= size ){ - this.visiblCellCount++; - } - - return true; -}; - -/** - * Checks whether the number of already rendered rows does not exceeds the number of rows visible in viewport + maximal - * number of rows rendered above and below viewport - * @returns {boolean} - */ -WalkontableRowStrategy.prototype.canRenderMoreRows = function () { - return this.remainingSize <= 0 || this.cellCount - this.visiblCellCount < this.curOuts; -}; - -WalkontableRowStrategy.prototype.remove = function () { - var size = this.cellSizes.pop(); - this.cellSizesSum -= size; - this.cellCount--; - this.remainingSize -= size; -}; - -WalkontableRowStrategy.prototype.removeOutstanding = function () { - while (this.cellCount - this.visiblCellCount > this.curOuts) { //this row is completely off screen! - this.remove(); - } -}; - -WalkontableRowStrategy.prototype.countRendered = function () { - return this.cellCount; -} - -WalkontableRowStrategy.prototype.countVisible = function () { - return this.visiblCellCount; -}; - -WalkontableRowStrategy.prototype.isLastIncomplete = function () { - var lastRow = this.instance.wtTable.getLastVisibleRow(); - var firstCol = this.instance.wtTable.getFirstVisibleColumn(); - var cell = this.instance.wtTable.getCell(new WalkontableCellCoords(lastRow, firstCol)); - var cellOffsetTop = Handsontable.Dom.offset(cell).top; - var cellHeight = Handsontable.Dom.outerHeight(cell); - var cellEnd = cellOffsetTop + cellHeight; - - var viewportOffsetTop = this.instance.wtScrollbars.horizontal.scrollHandler.offsetTop + this.instance.wtScrollbars.vertical.getScrollPosition(); - var viewportHeight = this.instance.wtViewport.getViewportHeight(); - var viewportEnd = viewportOffsetTop + viewportHeight; - - - return viewportEnd < cellEnd; -}; - -function WalkontableScroll(instance) { - this.instance = instance; -} - -WalkontableScroll.prototype.scrollVertical = function (delta) { - if (!this.instance.drawn) { - throw new Error('scrollVertical can only be called after table was drawn to DOM'); - } - - var instance = this.instance - , newOffset - , offset = instance.getSetting('offsetRow') - , fixedCount = instance.getSetting('fixedRowsTop') - , total = instance.getSetting('totalRows') - , maxSize = instance.wtViewport.getViewportHeight(); - - if (total > 0 && !this.instance.wtTable.isLastRowFullyVisible()) { - newOffset = this.scrollLogicVertical(delta, offset, total, fixedCount, maxSize, function (row) { - if (row - offset < fixedCount && row - offset >= 0) { - return instance.getSetting('rowHeight', row - offset); - } - else { - return instance.getSetting('rowHeight', row); - } - }); - - } else { - newOffset = 0; - } - - - if (newOffset !== offset) { - this.instance.wtScrollbars.vertical.scrollTo(newOffset); - } - return instance; -}; - -WalkontableScroll.prototype.scrollHorizontal = function (delta) { - this.instance.wtScrollbars.horizontal.scrollTo(delta); - return this.instance; -}; - -WalkontableScroll.prototype.scrollLogicVertical = function (delta, offset, total, fixedCount, maxSize, cellSizeFn) { - var newOffset = offset + delta; - - if (newOffset >= total - fixedCount) { - newOffset = total - fixedCount - 1; - } - - if (newOffset < 0) { - newOffset = 0; - } - - return newOffset; -}; - -/** - * Scrolls viewport to a cell by minimum number of cells - * @param {WalkontableCellCoords} coords - */ -WalkontableScroll.prototype.scrollViewport = function (coords) { - if (!this.instance.drawn) { - return; - } - - var offsetRow = this.instance.getSetting('offsetRow') - , totalRows = this.instance.getSetting('totalRows') - , totalColumns = this.instance.getSetting('totalColumns'); - - - if (coords.row < 0 || coords.row > totalRows - 1) { - throw new Error('row ' + coords.row + ' does not exist'); - } - - if (coords.col < 0 || coords.col > totalColumns - 1) { - throw new Error('column ' + coords.col + ' does not exist'); - } - - var TD = this.instance.wtTable.getCell(coords); - if (typeof TD === 'object') { - if (coords.col >= this.instance.getSetting('fixedColumnsLeft')) { - this.scrollToRenderedCell(TD); - } - } else if (coords.row >= this.instance.wtTable.getLastVisibleRow()) { - - this.scrollVertical(coords.row - this.instance.wtTable.getLastVisibleRow()); - - if (coords.row == this.instance.wtTable.getLastVisibleRow() && this.instance.wtTable.getRowStrategy().isLastIncomplete()){ - this.scrollViewport(coords) - } - - } else if (coords.row >= this.instance.getSetting('fixedRowsTop')){ - this.scrollVertical(coords.row - this.instance.wtTable.getFirstVisibleRow()); - } -}; - -WalkontableScroll.prototype.scrollToRenderedCell = function (TD) { - var cellOffset = Handsontable.Dom.offset(TD); - var cellWidth = Handsontable.Dom.outerWidth(TD); - var cellHeight = Handsontable.Dom.outerHeight(TD); - var workspaceOffset = Handsontable.Dom.offset(this.instance.wtTable.TABLE); - var viewportScrollPosition = { - left: this.instance.wtScrollbars.horizontal.getScrollPosition(), - top: this.instance.wtScrollbars.vertical.getScrollPosition() - }; - - var workspaceWidth = this.instance.wtViewport.getWorkspaceWidth(); - var workspaceHeight = this.instance.wtViewport.getWorkspaceHeight(); - var leftCloneWidth = Handsontable.Dom.outerWidth(this.instance.wtScrollbars.horizontal.clone.wtTable.TABLE); - var topCloneHeight = Handsontable.Dom.outerHeight(this.instance.wtScrollbars.vertical.clone.wtTable.TABLE); - - if (this.instance.wtScrollbars.horizontal.scrollHandler !== window) { - workspaceOffset.left = 0; - cellOffset.left -= Handsontable.Dom.offset(this.instance.wtScrollbars.horizontal.scrollHandler).left; - } - - if (this.instance.wtScrollbars.vertical.scrollHandler !== window) { - workspaceOffset.top = 0; - cellOffset.top = cellOffset.top - Handsontable.Dom.offset(this.instance.wtScrollbars.vertical.scrollHandler).top; - } - - if (cellWidth < workspaceWidth) { - if (cellOffset.left < viewportScrollPosition.left + leftCloneWidth) { - this.instance.wtScrollbars.horizontal.setScrollPosition(cellOffset.left - leftCloneWidth); - } - else if (cellOffset.left + cellWidth > workspaceOffset.left + viewportScrollPosition.left + workspaceWidth) { - var delta = (cellOffset.left + cellWidth) - (workspaceOffset.left + viewportScrollPosition.left + workspaceWidth); - this.instance.wtScrollbars.horizontal.setScrollPosition(viewportScrollPosition.left + delta); - } - } - - if (cellHeight < workspaceHeight) { - if (cellOffset.top < viewportScrollPosition.top + topCloneHeight) { - this.instance.wtScrollbars.vertical.setScrollPosition(cellOffset.top - topCloneHeight); - this.instance.wtScrollbars.vertical.onScroll(); - } - else if (cellOffset.top + cellHeight > viewportScrollPosition.top + workspaceHeight) { - this.instance.wtScrollbars.vertical.setScrollPosition(cellOffset.top - workspaceHeight + cellHeight); - this.instance.wtScrollbars.vertical.onScroll(); - } - } - -}; - -function WalkontableCornerScrollbarNative(instance) { - this.instance = instance; - this.type = 'corner'; - this.init(); - this.clone = this.makeClone('corner'); -} - -WalkontableCornerScrollbarNative.prototype = new WalkontableOverlay(); - -WalkontableCornerScrollbarNative.prototype.resetFixedPosition = function () { - if (!this.instance.wtTable.holder.parentNode) { - return; //removed from DOM - } - var elem = this.clone.wtTable.holder.parentNode; - - if (this.scrollHandler === window) { - var box = this.instance.wtTable.holder.getBoundingClientRect(); - var top = Math.ceil(box.top); - var left = Math.ceil(box.left); - - if (left < 0) { - elem.style.left = -left + 'px'; - } else { - elem.style.left = '0'; - } - - if (top < 0) { - elem.style.top = -top + "px"; - } else { - elem.style.top = "0"; - } - } - else { - elem.style.top = this.instance.wtScrollbars.vertical.windowScrollPosition + "px"; - elem.style.left = this.instance.wtScrollbars.horizontal.windowScrollPosition + "px"; - } - - elem.style.width = Handsontable.Dom.outerWidth(this.clone.wtTable.TABLE) + 4 + 'px'; - elem.style.height = Handsontable.Dom.outerHeight(this.clone.wtTable.TABLE) + 4 + 'px'; -}; - -WalkontableCornerScrollbarNative.prototype.refresh = function (selectionsOnly) { - WalkontableOverlay.prototype.refresh.call(this, selectionsOnly); -}; - -WalkontableCornerScrollbarNative.prototype.getScrollPosition = function () { -}; - -WalkontableCornerScrollbarNative.prototype.getLastCell = function () { -}; - -WalkontableCornerScrollbarNative.prototype.applyToDOM = function () { -}; - -WalkontableCornerScrollbarNative.prototype.scrollTo = function () { -}; - -WalkontableCornerScrollbarNative.prototype.readWindowSize = function () { -}; - -WalkontableCornerScrollbarNative.prototype.readSettings = function () { -}; -function WalkontableHorizontalScrollbarNative(instance) { - this.instance = instance; - this.type = 'horizontal'; - this.cellSize = 50; - this.offset = 0; - this.init(); - this.clone = this.makeClone('left'); -} - -WalkontableHorizontalScrollbarNative.prototype = new WalkontableOverlay(); - -//resetFixedPosition (in future merge it with this.refresh?) -WalkontableHorizontalScrollbarNative.prototype.resetFixedPosition = function () { - if (!this.instance.wtTable.holder.parentNode) { - return; //removed from DOM - } - var elem = this.clone.wtTable.holder.parentNode; - - if (this.scrollHandler === window) { - var box = this.instance.wtTable.holder.getBoundingClientRect(); - var left = Math.ceil(box.left); - - if (left < 0) { - elem.style.left = -left + 'px'; - } else { - elem.style.left = '0'; - } - - elem.style.top = this.instance.wtTable.hider.style.top; - } - else { - elem.style.top = this.instance.wtTable.hider.style.top; - elem.style.left = this.windowScrollPosition + "px"; - } - - elem.style.height = Handsontable.Dom.outerHeight(this.clone.wtTable.TABLE) + 'px'; - elem.style.width = Handsontable.Dom.outerWidth(this.clone.wtTable.TABLE) + 4 + 'px'; -}; - -WalkontableHorizontalScrollbarNative.prototype.refresh = function (selectionsOnly) { - WalkontableOverlay.prototype.refresh.call(this, selectionsOnly); -}; - -WalkontableHorizontalScrollbarNative.prototype.getScrollPosition = function () { - return Handsontable.Dom.getScrollLeft(this.scrollHandler); -}; - -WalkontableHorizontalScrollbarNative.prototype.setScrollPosition = function (pos) { - if (this.scrollHandler === window){ - window.scrollTo(pos, Handsontable.Dom.getWindowScrollTop()); - } else { - this.scrollHandler.scrollLeft = pos; - } -}; - -WalkontableHorizontalScrollbarNative.prototype.onScroll = function () { - WalkontableOverlay.prototype.onScroll.call(this); - - this.instance.getSetting('onScrollHorizontally'); -}; - -WalkontableHorizontalScrollbarNative.prototype.getLastCell = function () { - return this.instance.wtTable.getLastVisibleColumn(); -}; - -//applyToDOM (in future merge it with this.refresh?) -WalkontableHorizontalScrollbarNative.prototype.applyToDOM = function () { - this.readWindowSize(); -}; - -WalkontableHorizontalScrollbarNative.prototype.scrollTo = function (cell) { - this.setScrollPosition(this.tableParentOffset + cell * this.cellSize); -}; - -WalkontableHorizontalScrollbarNative.prototype.readWindowSize = function () { - if (this.scrollHandler === window) { - this.windowSize = document.documentElement.clientWidth; - this.tableParentOffset = this.instance.wtTable.holderOffset.left; - } - else { - this.windowSize = this.scrollHandler.clientWidth; - this.tableParentOffset = 0; - } - this.windowScrollPosition = this.getScrollPosition(); -}; - -WalkontableHorizontalScrollbarNative.prototype.readSettings = function () { - this.readWindowSize(); - this.total = this.instance.getSetting('totalColumns'); -}; -function WalkontableVerticalScrollbarNative(instance) { - this.instance = instance; - this.type = 'vertical'; - this.cellSize = this.instance.wtSettings.settings.defaultRowHeight; - this.offset; - this.total; - this.init(); - this.clone = this.makeClone('top'); -} - -WalkontableVerticalScrollbarNative.prototype = new WalkontableOverlay(); - -//resetFixedPosition (in future merge it with this.refresh?) -WalkontableVerticalScrollbarNative.prototype.resetFixedPosition = function () { - if (!this.instance.wtTable.holder.parentNode) { - return; //removed from DOM - } - var elem = this.clone.wtTable.holder.parentNode; - - if (this.scrollHandler === window) { - var box = this.instance.wtTable.holder.getBoundingClientRect(); - var top = Math.ceil(box.top); - - elem.style.left = '0'; - - if (top < 0) { - elem.style.top = -top + "px"; - } else { - elem.style.top = "0"; - } - } - else { - elem.style.top = this.windowScrollPosition + "px"; - elem.style.left = '0'; - } - - if (this.instance.wtScrollbars.horizontal.scrollHandler === window) { - elem.style.width = this.instance.wtViewport.getWorkspaceActualWidth() + 'px'; - } - else { - elem.style.width = Handsontable.Dom.outerWidth(this.clone.wtTable.TABLE) + 'px'; - } - - elem.style.height = Handsontable.Dom.outerHeight(this.clone.wtTable.TABLE) + 4 + 'px'; -}; - -WalkontableVerticalScrollbarNative.prototype.getScrollPosition = function () { - return Handsontable.Dom.getScrollTop(this.scrollHandler); -}; - -WalkontableVerticalScrollbarNative.prototype.setScrollPosition = function (pos) { - if (this.scrollHandler === window){ - window.scrollTo(Handsontable.Dom.getWindowScrollLeft(), pos); - } else { - this.scrollHandler.scrollTop = pos; - } -}; - -WalkontableVerticalScrollbarNative.prototype.onScroll = function () { - WalkontableOverlay.prototype.onScroll.call(this); - - this.instance.draw(true);// - - this.instance.getSetting('onScrollVertically'); -}; - -WalkontableVerticalScrollbarNative.prototype.getLastCell = function () { - return this.instance.getSetting('offsetRow') + this.instance.wtTable.tbodyChildrenLength - 1; -}; - -WalkontableVerticalScrollbarNative.prototype.sumCellSizes = function (from, length) { - var sum = 0; - while (from < length) { - sum += this.instance.wtSettings.settings.rowHeight(from) || this.instance.wtSettings.settings.defaultRowHeight; //TODO optimize getSetting, because this is MUCH faster then getSetting - from++; - } - return sum; -}; - -WalkontableVerticalScrollbarNative.prototype.refresh = function (selectionsOnly) { - this.applyToDOM(); - WalkontableOverlay.prototype.refresh.call(this, selectionsOnly); -}; - -//applyToDOM (in future merge it with this.refresh?) -WalkontableVerticalScrollbarNative.prototype.applyToDOM = function () { - var last = this.getLastCell(); - this.measureBefore = this.sumCellSizes(0, this.offset); - if (last === -1) { //last -1 means that viewport is scrolled behind the table - this.measureAfter = 0; - } - else { - this.measureAfter = this.sumCellSizes(last, this.total - last); - } - var headerSize = this.instance.wtViewport.getColumnHeaderHeight(); - this.fixedContainer.style.height = headerSize + this.sumCellSizes(0, this.total) + 8 + 'px'; //+4 is needed, otherwise vertical scroll appears in Chrome (window scroll mode) - maybe because of fill handle in last row or because of box shadow - this.fixed.style.top = this.measureBefore + 'px'; - this.fixed.style.bottom = ''; -}; - -WalkontableVerticalScrollbarNative.prototype.scrollTo = function (cell) { - var newY = this.tableParentOffset + cell * this.cellSize; - this.setScrollPosition(newY); - this.onScroll(); -}; - -WalkontableVerticalScrollbarNative.prototype.readWindowSize = function () { - if (this.scrollHandler === window) { - this.windowSize = document.documentElement.clientHeight; - this.tableParentOffset = this.instance.wtTable.holderOffset.top; - } - else { - var elemHeight = Handsontable.Dom.outerHeight(this.scrollHandler); - this.windowSize = elemHeight > 0 && this.scrollHandler.clientHeight > 0 ? this.scrollHandler.clientHeight : Infinity; //returns height without DIV scrollbar - this.tableParentOffset = 0; - } - this.windowScrollPosition = this.getScrollPosition(); -}; - -WalkontableVerticalScrollbarNative.prototype.readSettings = function () { - this.readWindowSize(); - - this.offset = this.instance.getSetting('offsetRow'); - this.total = this.instance.getSetting('totalRows'); - - var scrollDelta = this.windowScrollPosition - this.tableParentOffset; - - var sum = 0; - var last; - for (var i = 0; i < this.total; i++) { - last = this.instance.getSetting('rowHeight', i) || this.instance.wtSettings.settings.defaultRowHeight; - sum += last; - if (sum - 1 > scrollDelta) { - break; - } - } - - this.offset = Math.min(i, this.total); - this.instance.update('offsetRow', this.offset); -}; -function WalkontableScrollbars(instance) { - this.instance = instance; - instance.update('scrollbarWidth', Handsontable.Dom.getScrollbarWidth()); - instance.update('scrollbarHeight', Handsontable.Dom.getScrollbarWidth()); - this.vertical = new WalkontableVerticalScrollbarNative(instance); - this.horizontal = new WalkontableHorizontalScrollbarNative(instance); - this.corner = new WalkontableCornerScrollbarNative(instance); - if (instance.getSetting('debug')) { - this.debug = new WalkontableDebugOverlay(instance); - } - this.registerListeners(); -} - -WalkontableScrollbars.prototype.registerListeners = function () { - var that = this; - - var oldVerticalScrollPosition - , oldHorizontalScrollPosition - , oldBoxTop - , oldBoxLeft; - - function refreshAll() { - if(!that.instance.drawn) { - return; - } - - if (!that.instance.wtTable.holder.parentNode) { - //Walkontable was detached from DOM, but this handler was not removed - that.destroy(); - return; - } - - that.vertical.windowScrollPosition = that.vertical.getScrollPosition(); - that.horizontal.windowScrollPosition = that.horizontal.getScrollPosition(); - that.box = that.instance.wtTable.hider.getBoundingClientRect(); - - if (that.vertical.windowScrollPosition !== oldVerticalScrollPosition || that.horizontal.windowScrollPosition !== oldHorizontalScrollPosition || that.box.top !== oldBoxTop || that.box.left !== oldBoxLeft) { - that.vertical.onScroll(); - that.horizontal.onScroll(); //it's done here to make sure that all onScroll's are executed before changing styles - that.corner.onScroll(); - - oldVerticalScrollPosition = that.vertical.windowScrollPosition; - oldHorizontalScrollPosition = that.horizontal.windowScrollPosition; - oldBoxTop = that.box.top; - oldBoxLeft = that.box.left; - } - } - - var $window = $(window); - this.vertical.$scrollHandler.on('scroll.' + this.instance.guid, refreshAll); - if (this.vertical.scrollHandler !== this.horizontal.scrollHandler) { - this.horizontal.$scrollHandler.on('scroll.' + this.instance.guid, refreshAll); - } - - if (this.vertical.scrollHandler !== window && this.horizontal.scrollHandler !== window) { - $window.on('scroll.' + this.instance.guid, refreshAll); - } -}; - -WalkontableScrollbars.prototype.destroy = function () { - if (this.vertical) { - this.vertical.destroy(); - this.vertical.$scrollHandler.off('scroll.' + this.instance.guid); - } - if (this.horizontal) { - this.horizontal.destroy(); - this.vertical.$scrollHandler.off('scroll.' + this.instance.guid); - } - $(window).off('scroll.' + this.instance.guid); - this.corner && this.corner.destroy(); - this.debug && this.debug.destroy(); -}; - -WalkontableScrollbars.prototype.refresh = function (selectionsOnly) { - this.horizontal && this.horizontal.readSettings(); - this.vertical && this.vertical.readSettings(); - this.horizontal && this.horizontal.refresh(selectionsOnly); - this.vertical && this.vertical.refresh(selectionsOnly); - this.corner && this.corner.refresh(selectionsOnly); - this.debug && this.debug.refresh(selectionsOnly); -}; - -WalkontableScrollbars.prototype.applyToDOM = function () { - this.horizontal && this.horizontal.applyToDOM(); - this.vertical && this.vertical.applyToDOM(); - this.corner && this.corner.applyToDOM(); - this.debug && this.debug.applyToDOM(); -}; -function WalkontableSelection(instance, settings) { - this.instance = instance; - this.settings = settings; - this.cellRange = null; - if (settings.border) { - this.border = new WalkontableBorder(instance, settings); - } -} - -/** - * Returns boolean information if selection is empty - * @returns {boolean} - */ -WalkontableSelection.prototype.isEmpty = function () { - return (this.cellRange === null); -}; - -/** - * Adds a cell coords to the selection - * @param {WalkontableCellCoords} coords - */ -WalkontableSelection.prototype.add = function (coords) { - if (this.isEmpty()) { - this.cellRange = new WalkontableCellRange(coords, coords, coords); - } - else { - this.cellRange.expand(coords); - } -}; - -/** - * If selection range from or to property equals oldCoords, replace it with newCoords. Return boolean information about success - * @param {WalkontableCellCoords} oldCoords - * @param {WalkontableCellCoords} newCoords - * @return {boolean} - */ -WalkontableSelection.prototype.replace = function (oldCoords, newCoords) { - if (!this.isEmpty()) { - if(this.cellRange.from.isEqual(oldCoords)) { - this.cellRange.from = newCoords; - return true; - } - if(this.cellRange.to.isEqual(oldCoords)) { - this.cellRange.to = newCoords; - return true; - } - } - return false; -}; - -WalkontableSelection.prototype.clear = function () { - this.cellRange = null; -}; - -/** - * Returns the top left (TL) and bottom right (BR) selection coordinates - * @returns {Object} - */ -WalkontableSelection.prototype.getCorners = function () { - var topLeft = this.cellRange.getTopLeftCorner(); - var bottomRight = this.cellRange.getBottomRightCorner(); - return [topLeft.row, topLeft.col, bottomRight.row, bottomRight.col]; -}; - -WalkontableSelection.prototype.draw = function () { - var corners, r, c, source_r, source_c, - instance = this.instance, - visibleRows = instance.wtTable.getRowStrategy().countVisible(), - renderedColumns = instance.wtTable.getColumnStrategy().cellCount, - cacheLength; - - if (!this.isEmpty()) { - corners = this.getCorners(); - - for (r = 0; r < visibleRows; r++) { - for (c = 0; c < renderedColumns; c++) { - source_r = instance.wtTable.rowFilter.visibleToSource(r); - source_c = instance.wtTable.columnFilter.visibleToSource(c); - - if (source_r >= corners[0] && source_r <= corners[2] && source_c >= corners[1] && source_c <= corners[3]) { - //selected cell - if (this.settings.className) { - instance.wtTable.currentCellCache.add(r, c, this.settings.className); - } - } - else if (source_r >= corners[0] && source_r <= corners[2]) { - //selection is in this row - instance.wtTable.currentCellCache.add(r, c, this.settings.highlightRowClassName); - - // selected row headers - instance.wtTable.currentCellCache.add(r,renderedColumns,this.settings.highlightRowClassName); - } - else if (source_c >= corners[1] && source_c <= corners[3]) { - //selection is in this column - instance.wtTable.currentCellCache.add(r, c, this.settings.highlightColumnClassName); - - // selected column headers - instance.wtTable.currentCellCache.add(visibleRows,c,this.settings.highlightColumnClassName); - } - } - } - - this.border && this.border.appear(corners); //warning! border.appear modifies corners! - } - else { - this.border && this.border.disappear(); - } -}; - -/* - Make a clone of a selection by overriding the WOT instance and creating new WalkontableBorder for the new instance - Method is used for creating selections in overlays - */ -WalkontableSelection.prototype.makeClone = function (instance) { - function WalkontableSelectionClone(){} - WalkontableSelectionClone.prototype = this; - - var clone = new WalkontableSelectionClone(); - - clone.instance = instance; - - if (clone.border){ - clone.border = new WalkontableBorder(instance, clone.settings); - } - - return clone; - -}; - -function WalkontableSettings(instance, settings) { - var that = this; - this.instance = instance; - - //default settings. void 0 means it is required, null means it can be empty - this.defaults = { - table: void 0, - debug: false, //shows WalkontableDebugOverlay - - //presentation mode - stretchH: 'none', //values: all, last, none - currentRowClassName: null, - currentColumnClassName: null, - - //data source - data: void 0, - offsetRow: 0, - fixedColumnsLeft: 0, - fixedRowsTop: 0, - rowHeaders: function () { - return [] - }, //this must be array of functions: [function (row, TH) {}] - columnHeaders: function () { - return [] - }, //this must be array of functions: [function (column, TH) {}] - totalRows: void 0, - totalColumns: void 0, - width: null, - height: null, - cellRenderer: function (row, column, TD) { - var cellData = that.getSetting('data', row, column); - Handsontable.Dom.fastInnerText(TD, cellData === void 0 || cellData === null ? '' : cellData); - }, - columnWidth: 50, - rowHeight: function (row) { - return 23; - }, - defaultRowHeight: 23, - selections: null, - hideBorderOnMouseDownOver: false, - - //callbacks - onCellMouseDown: null, - onCellMouseOver: null, -// onCellMouseOut: null, - onCellDblClick: null, - onCellCornerMouseDown: null, - onCellCornerDblClick: null, - beforeDraw: null, - onDraw: null, - onScrollVertically: null, - onScrollHorizontally: null, - - //constants - scrollbarWidth: 10, - scrollbarHeight: 10, - - renderAllRows: false - }; - - //reference to settings - this.settings = {}; - for (var i in this.defaults) { - if (this.defaults.hasOwnProperty(i)) { - if (settings[i] !== void 0) { - this.settings[i] = settings[i]; - } - else if (this.defaults[i] === void 0) { - throw new Error('A required setting "' + i + '" was not provided'); - } - else { - this.settings[i] = this.defaults[i]; - } - } - } -} - -/** - * generic methods - */ - -WalkontableSettings.prototype.update = function (settings, value) { - if (value === void 0) { //settings is object - for (var i in settings) { - if (settings.hasOwnProperty(i)) { - this.settings[i] = settings[i]; - } - } - } - else { //if value is defined then settings is the key - this.settings[settings] = value; - } - return this.instance; -}; - -WalkontableSettings.prototype.getSetting = function (key, param1, param2, param3, param4) { - if (typeof this.settings[key] === 'function') { - return this.settings[key](param1, param2, param3, param4); //this is faster than .apply - https://github.com/handsontable/jquery-handsontable/wiki/JavaScript-&-DOM-performance-tips - } - else if (param1 !== void 0 && Object.prototype.toString.call(this.settings[key]) === '[object Array]') { //perhaps this can be removed, it is only used in tests - return this.settings[key][param1]; - } - else { - return this.settings[key]; - } -}; - -WalkontableSettings.prototype.has = function (key) { - return !!this.settings[key] -}; -function WalkontableTable(instance, table) { - //reference to instance - this.instance = instance; - this.TABLE = table; - Handsontable.Dom.removeTextNodes(this.TABLE); - - //wtSpreader - var parent = this.TABLE.parentNode; - if (!parent || parent.nodeType !== 1 || !Handsontable.Dom.hasClass(parent, 'wtHolder')) { - var spreader = document.createElement('DIV'); - spreader.className = 'wtSpreader'; - if (parent) { - parent.insertBefore(spreader, this.TABLE); //if TABLE is detached (e.g. in Jasmine test), it has no parentNode so we cannot attach holder to it - } - spreader.appendChild(this.TABLE); - } - this.spreader = this.TABLE.parentNode; - - //wtHider - parent = this.spreader.parentNode; - if (!parent || parent.nodeType !== 1 || !Handsontable.Dom.hasClass(parent, 'wtHolder')) { - var hider = document.createElement('DIV'); - hider.className = 'wtHider'; - if (parent) { - parent.insertBefore(hider, this.spreader); //if TABLE is detached (e.g. in Jasmine test), it has no parentNode so we cannot attach holder to it - } - hider.appendChild(this.spreader); - } - this.hider = this.spreader.parentNode; - this.hiderStyle = this.hider.style; - this.hiderStyle.position = 'relative'; - - //wtHolder - parent = this.hider.parentNode; - if (!parent || parent.nodeType !== 1 || !Handsontable.Dom.hasClass(parent, 'wtHolder')) { - var holder = document.createElement('DIV'); - holder.style.position = 'relative'; - holder.className = 'wtHolder'; - - if(!instance.cloneSource) { - holder.className += ' ht_master'; - } - - if (parent) { - parent.insertBefore(holder, this.hider); //if TABLE is detached (e.g. in Jasmine test), it has no parentNode so we cannot attach holder to it - } - holder.appendChild(this.hider); - } - this.holder = this.hider.parentNode; - - //bootstrap from settings - this.TBODY = this.TABLE.getElementsByTagName('TBODY')[0]; - if (!this.TBODY) { - this.TBODY = document.createElement('TBODY'); - this.TABLE.appendChild(this.TBODY); - } - this.THEAD = this.TABLE.getElementsByTagName('THEAD')[0]; - if (!this.THEAD) { - this.THEAD = document.createElement('THEAD'); - this.TABLE.insertBefore(this.THEAD, this.TBODY); - } - this.COLGROUP = this.TABLE.getElementsByTagName('COLGROUP')[0]; - if (!this.COLGROUP) { - this.COLGROUP = document.createElement('COLGROUP'); - this.TABLE.insertBefore(this.COLGROUP, this.THEAD); - } - - if (this.instance.getSetting('columnHeaders').length) { - if (!this.THEAD.childNodes.length) { - var TR = document.createElement('TR'); - this.THEAD.appendChild(TR); - } - } - - this.colgroupChildrenLength = this.COLGROUP.childNodes.length; - this.theadChildrenLength = this.THEAD.firstChild ? this.THEAD.firstChild.childNodes.length : 0; - this.tbodyChildrenLength = this.TBODY.childNodes.length; - - this.oldCellCache = new WalkontableClassNameCache(); - this.currentCellCache = new WalkontableClassNameCache(); - - this.rowFilter = null; - this.columnFilter = null; - - this.columnWidthCache = []; -} - -WalkontableTable.prototype.getRowStrategy = function () { - return this.isWorkingOnClone() ? this.instance.cloneSource.wtTable.rowStrategy : this.rowStrategy; -}; - -WalkontableTable.prototype.getColumnStrategy = function () { - return this.isWorkingOnClone() ? this.instance.cloneSource.wtTable.columnStrategy : this.columnStrategy; -}; - -WalkontableTable.prototype.isWorkingOnClone = function () { - return !!this.instance.cloneSource; -}; - -WalkontableTable.prototype.refreshHiderDimensions = function () { - var spreaderStyle = this.spreader.style; - spreaderStyle.position = 'relative'; - spreaderStyle.width = 'auto'; - spreaderStyle.height = 'auto'; -}; - -WalkontableTable.prototype.draw = function (selectionsOnly) { - if (!selectionsOnly) { - if (this.isWorkingOnClone()) { - this.tableOffset = this.instance.cloneSource.wtTable.tableOffset; - } - else { - this.holderOffset = Handsontable.Dom.offset(this.holder); - this.tableOffset = Handsontable.Dom.offset(this.TABLE); - this.instance.wtScrollbars.vertical.readSettings(); - this.instance.wtScrollbars.horizontal.readSettings(); - this.instance.wtViewport.resetSettings(); - } - var offsetRow; - if (this.instance.cloneOverlay instanceof WalkontableDebugOverlay - || this.instance.cloneOverlay instanceof WalkontableVerticalScrollbarNative - || this.instance.cloneOverlay instanceof WalkontableCornerScrollbarNative) { - offsetRow = 0; - } - else { - offsetRow = this.instance.wtSettings.settings.offsetRow; - } - - this.rowFilter = new WalkontableRowFilter( - offsetRow, - this.instance.getSetting('totalRows'), - this.instance.getSetting('columnHeaders').length - ); - this.columnFilter = new WalkontableColumnFilter( - this.instance.getSetting('totalColumns'), - this.instance.getSetting('rowHeaders').length - ); - this._doDraw(); - } - else { - this.instance.wtScrollbars && this.instance.wtScrollbars.refresh(true); - } - - this.refreshPositions(selectionsOnly); - - if (!selectionsOnly) { - if (!this.isWorkingOnClone()) { - this.instance.wtScrollbars.vertical.resetFixedPosition(); - this.instance.wtScrollbars.horizontal.resetFixedPosition(); - this.instance.wtScrollbars.corner.resetFixedPosition(); - this.instance.wtScrollbars.debug && this.instance.wtScrollbars.debug.resetFixedPosition(); - } - } - - this.instance.drawn = true; - return this; -}; - -WalkontableTable.prototype._doDraw = function () { - var wtRenderer = new WalkontableTableRenderer(this); - wtRenderer.render(); -}; - -WalkontableTable.prototype.refreshPositions = function (selectionsOnly) { - this.refreshHiderDimensions(); - this.refreshSelections(selectionsOnly); -}; - -WalkontableTable.prototype.refreshSelections = function (selectionsOnly) { - var vr - , r - , vc - , c - , s - , slen - , classNames = [] - , visibleRows = this.getRowStrategy().countVisible() - , renderedCells = this.getColumnStrategy().cellCount - , cacheLength; - - - this.oldCellCache = this.currentCellCache; - this.currentCellCache = new WalkontableClassNameCache(); - - if (this.instance.selections) { - for (var i = 0, ilen = this.instance.selections.length; i < ilen; i++) { - this.instance.selections[i].draw(); - - if (this.instance.selections[i].settings.className) { - classNames.push(this.instance.selections[i].settings.className); - } - if (this.instance.selections[i].settings.highlightRowClassName) { - classNames.push(this.instance.selections[i].settings.highlightRowClassName); - } - if (this.instance.selections[i].settings.highlightColumnClassName) { - classNames.push(this.instance.selections[i].settings.highlightColumnClassName); - } - } - } - - slen = classNames.length; - - for (vr = 0; vr < visibleRows; vr++) { - for (vc = 0; vc < renderedCells; vc++) { - r = this.rowFilter.visibleToSource(vr); - c = this.columnFilter.visibleToSource(vc); - for (s = 0; s < slen; s++) { - var cell; - if (this.currentCellCache.test(vr, vc, classNames[s])) { - cell = this.getCell(new WalkontableCellCoords(r, c)); - if (typeof cell == 'object' ) Handsontable.Dom.addClass(cell, classNames[s]); - } - else if (selectionsOnly && this.oldCellCache.test(vr, vc, classNames[s])) { - cell = this.getCell(new WalkontableCellCoords(r, c)); - if (typeof cell == 'object' ) Handsontable.Dom.removeClass(cell, classNames[s]); - - } - - // for headers: - // column headers - cacheLength = this.currentCellCache.cache ? visibleRows : 0; - cell = this.getColumnHeader(vc); - if (this.currentCellCache.test(cacheLength, vc, classNames[s])) { - if (typeof cell == 'object' ) Handsontable.Dom.addClass(cell,classNames[s]); - } else { - if (typeof cell == 'object' ) Handsontable.Dom.removeClass(cell,classNames[s]); - } - - // row headers - cacheLength = this.currentCellCache.cache[vr] ? renderedCells : 0; - cell = this.getRowHeader(vr) != -1 ? this.getRowHeader(vr) : undefined; - - if (this.currentCellCache.test(vr, cacheLength, classNames[s])) { - if (typeof cell == 'object' ) Handsontable.Dom.addClass(cell,classNames[s]); - } else { - if (typeof cell == 'object' ) Handsontable.Dom.removeClass(cell,classNames[s]); - } - - } - } - } - -}; - -/** - * getCell - * @param {WalkontableCellCoords} coords - * @return {Object} HTMLElement on success or {Number} one of the exit codes on error: - * -1 row before viewport - * -2 row after viewport - * - */ -WalkontableTable.prototype.getCell = function (coords) { - if (this.isRowBeforeViewport(coords.row)) { - return -1; //row before viewport - } - else if (this.isRowAfterViewport(coords.row)) { - return -2; //row after viewport - } - - var TR = this.TBODY.childNodes[this.rowFilter.sourceToVisible(coords.row)]; - - if (TR) { - return TR.childNodes[this.columnFilter.sourceColumnToVisibleRowHeadedColumn(coords.col)]; - } -}; - -/** - * getColumnHeader - * @param col - * @return {Object} HTMLElement on success or undefined on error - * - */ -WalkontableTable.prototype.getColumnHeader = function(col) { - var THEAD = this.THEAD.childNodes[0]; - if (THEAD) { - return THEAD.childNodes[this.columnFilter.sourceColumnToVisibleRowHeadedColumn(col)]; - } -} - -/** - * getRowHeader - * @param col - * @return {Object} HTMLElement on success or {Number} one of the exit codes on error: - * -1 table doesn't have row headers - * - */ -WalkontableTable.prototype.getRowHeader = function(row) { - if(this.columnFilter.sourceColumnToVisibleRowHeadedColumn(0) == 0) { - return -1; - } - - var TR = this.TBODY.childNodes[this.rowFilter.sourceToVisible(row)]; - - if (TR) { - return TR.childNodes[0]; - } -} - -/** - * Returns cell coords object for a given TD - * @param TD - * @returns {WalkontableCellCoords} - */ -WalkontableTable.prototype.getCoords = function (TD) { - var TR = TD.parentNode; - var row = Handsontable.Dom.index(TR); - if (TR.parentNode === this.THEAD) { - row = this.rowFilter.visibleColHeadedRowToSourceRow(row); - } - else { - row = this.rowFilter.visibleToSource(row); - } - - return new WalkontableCellCoords( - row, - this.columnFilter.visibleRowHeadedColumnToSourceColumn(TD.cellIndex) - ); -}; - -WalkontableTable.prototype.getTrForRow = function (row) { - return this.TBODY.childNodes[this.rowFilter.sourceToVisible(row)]; -}; - -//returns -1 if no row is visible -WalkontableTable.prototype.getFirstVisibleRow = function () { - return this.rowFilter.visibleToSource(0); -}; - -//returns -1 if no column is visible -WalkontableTable.prototype.getFirstVisibleColumn = function () { - - if (this.isWorkingOnClone()){ - if (this.instance.cloneOverlay instanceof WalkontableHorizontalScrollbarNative || this.instance.cloneOverlay instanceof WalkontableCornerScrollbarNative){ - return 0; - } else { - return this.instance.cloneSource.wtTable.getFirstVisibleColumn(); - } - } - - var leftOffset = this.instance.wtScrollbars.horizontal.getScrollPosition(); - var columnCount = this.getColumnStrategy().cellCount; - var firstTR = this.TBODY.firstChild; - - if (!firstTR){ - return 0; - } - - for (var colIndex = 0; colIndex < columnCount; colIndex++){ - leftOffset -= firstTR.childNodes[colIndex].offsetWidth; - - if (leftOffset < 0){ - return colIndex; - } - - } - - return -1; -}; - -//returns -1 if no row is visible -WalkontableTable.prototype.getLastVisibleRow = function () { - var lastVisibleRow = this.rowFilter.visibleToSource(this.getRowStrategy().countVisible() - 1); - var instance = this.instance; - - if (instance.cloneOverlay instanceof WalkontableVerticalScrollbarNative || instance.cloneOverlay instanceof WalkontableCornerScrollbarNative) { - var fixedRowsTop = this.instance.getSetting('fixedRowsTop'); - - return Math.min(fixedRowsTop - 1, lastVisibleRow); - } else { - return lastVisibleRow; - } - -}; - -//returns -1 if no column is visible -WalkontableTable.prototype.getLastVisibleColumn = function () { - var instance = this.instance; - - if (this.isWorkingOnClone()){ - - if (instance.cloneOverlay instanceof WalkontableHorizontalScrollbarNative || instance.cloneOverlay instanceof WalkontableCornerScrollbarNative){ - var lastVisibleColumn = this.getColumnStrategy().countVisible() - 1; - var fixedColumnsLeft = instance.getSetting('fixedColumnsLeft'); - return Math.min(fixedColumnsLeft - 1, lastVisibleColumn); - } else { - return this.instance.cloneSource.wtTable.getLastVisibleColumn(); - } - - } - - - var leftOffset = this.instance.wtScrollbars.horizontal.getScrollPosition(); - var leftPartOfTable = leftOffset + this.instance.wtViewport.getWorkspaceWidth(Infinity); - var columnCount = this.getColumnStrategy().cellCount; - var rowHeaderCount = this.instance.getSetting('rowHeaders').length || 0; - var firstTR = this.TBODY.firstChild; - - if (!columnCount) { - return -1; - } - - for (var colIndex = 0; colIndex < columnCount + rowHeaderCount; colIndex++){ - leftPartOfTable -= firstTR.childNodes[colIndex].offsetWidth; - - if (leftPartOfTable <= 0){ - return colIndex - rowHeaderCount; - } - - } - - return colIndex - rowHeaderCount - 1; -}; - -WalkontableTable.prototype.isRowBeforeViewport = function (r) { - return (this.rowFilter.sourceToVisible(r) < 0 && r >= 0); -}; - -WalkontableTable.prototype.isRowAfterViewport = function (r) { - return (r > this.getLastVisibleRow()); -}; - -WalkontableTable.prototype.isColumnBeforeViewport = function (c) { - return (this.columnFilter.sourceToVisible(c) < 0 && c >= 0); -}; - -WalkontableTable.prototype.isColumnAfterViewport = function (c) { - return (c > this.getLastVisibleColumn()); -}; - -WalkontableTable.prototype.isRowInViewport = function (r) { - return (!this.isRowBeforeViewport(r) && !this.isRowAfterViewport(r)); -}; - -WalkontableTable.prototype.isColumnInViewport = function (c) { - return (!this.isColumnBeforeViewport(c) && !this.isColumnAfterViewport(c)); -}; - -WalkontableTable.prototype.isLastRowFullyVisible = function () { - return (this.getLastVisibleRow() === this.instance.getSetting('totalRows') - 1 && !this.getRowStrategy().isLastIncomplete()); -}; - -WalkontableTable.prototype.isLastColumnFullyVisible = function () { - return (this.getLastVisibleColumn() === this.instance.getSetting('totalColumns') - 1 && !this.getColumnStrategy().isLastIncomplete()); -}; - -WalkontableTable.prototype.getVisibleRowsCount = function () { - return this.getRowStrategy().countVisible(); -}; - -WalkontableTable.prototype.allRowsInViewport = function () { - return this.getRowStrategy().cellCount == this.getVisibleRowsCount(); -}; - -function WalkontableTableRenderer(wtTable){ - this.wtTable = wtTable; - this.instance = wtTable.instance; - this.rowFilter = wtTable.rowFilter; - this.columnFilter = wtTable.columnFilter; - - this.TABLE = wtTable.TABLE; - this.THEAD = wtTable.THEAD; - this.TBODY = wtTable.TBODY; - this.COLGROUP = wtTable.COLGROUP; - - this.utils = WalkontableTableRenderer.utils; - -} - - WalkontableTableRenderer.prototype.render = function () { - if (!this.wtTable.isWorkingOnClone()) { - this.instance.getSetting('beforeDraw', true); - } - - this.rowHeaders = this.instance.getSetting('rowHeaders'); - this.rowHeaderCount = this.rowHeaders.length; - this.fixedRowsTop = this.instance.getSetting('fixedRowsTop'); - this.columnHeaders = this.instance.getSetting('columnHeaders'); - - var visibleColIndex - , totalRows = this.instance.getSetting('totalRows') - , totalColumns = this.instance.getSetting('totalColumns') - , displayTds - , TR - , TD - , TH - , adjusted = false - , workspaceWidth - , res; - - if (totalColumns > 0) { - var cloneLimit; - if (this.wtTable.isWorkingOnClone()) { //must be run after adjustAvailableNodes because otherwise this.rowStrategy is not yet defined - if (this.instance.cloneOverlay instanceof WalkontableVerticalScrollbarNative || this.instance.cloneOverlay instanceof WalkontableCornerScrollbarNative) { - cloneLimit = this.fixedRowsTop; - } - else if (this.instance.cloneOverlay instanceof WalkontableHorizontalScrollbarNative) { - cloneLimit = this.wtTable.getRowStrategy().cellCount; - } - //else if WalkontableDebugOverlay do nothing. No cloneLimit means render ALL rows - } - - this.adjustAvailableNodes(); - adjusted = true; - - this.renderColGroups(); - - this.renderColumnHeaders(); - - displayTds = this.getColumnCount(); - - //Render table rows - this.renderRows(totalRows, cloneLimit, displayTds); - - if (!this.wtTable.isWorkingOnClone()) { - workspaceWidth = this.instance.wtViewport.getWorkspaceWidth(); - this.instance.wtViewport.containerWidth = null; - this.wtTable.getColumnStrategy().stretch(); - } - - this.adjustColumnWidths(displayTds); - } - - if (!adjusted) { - this.adjustAvailableNodes(); - } - - if (!(this.instance.cloneOverlay instanceof WalkontableDebugOverlay)) { - this.removeRedundantRows(); - } - - if (!this.wtTable.isWorkingOnClone()) { - this.markOversizedRows(); - - this.instance.wtScrollbars.applyToDOM(); - - if (workspaceWidth !== this.instance.wtViewport.getWorkspaceWidth()) { - //workspace width changed though to shown/hidden vertical scrollbar. Let's reapply stretching - this.instance.wtViewport.containerWidth = null; - this.wtTable.getColumnStrategy().stretch(); - var cache = this.instance.wtTable.columnWidthCache; - for (visibleColIndex = 0; visibleColIndex < this.wtTable.getColumnStrategy().cellCount; visibleColIndex++) { - var width = this.wtTable.getColumnStrategy().getSize(visibleColIndex); - this.COLGROUP.childNodes[visibleColIndex + this.rowHeaderCount].style.width = width + 'px'; - cache[visibleColIndex] = width; - } - } - - this.instance.wtScrollbars.refresh(false); - - this.instance.getSetting('onDraw', true); - } - -}; - -WalkontableTableRenderer.prototype.removeRedundantRows = function () { - var renderedRowIndex = this.wtTable.getRowStrategy().countRendered(); - while (this.wtTable.tbodyChildrenLength > renderedRowIndex) { - this.TBODY.removeChild(this.TBODY.lastChild); - this.wtTable.tbodyChildrenLength--; - } -}; - -WalkontableTableRenderer.prototype.renderRows = function (totalRows, cloneLimit, displayTds) { - var lastTD, TR, res; - var offsetRow = this.instance.getSetting('offsetRow'); - var visibleRowIndex = 0; - var sourceRowIndex = this.rowFilter.visibleToSource(visibleRowIndex); - var isWorkingOnClone = this.wtTable.isWorkingOnClone(); - - while (sourceRowIndex < totalRows && sourceRowIndex >= 0) { - if (visibleRowIndex > 1000) { - throw new Error('Security brake: Too much TRs. Please define height for your table, which will enforce scrollbars.'); - } - - if (cloneLimit !== void 0 && visibleRowIndex === cloneLimit) { - break; //we have as much rows as needed for this clone - } - - TR = this.getOrCreateTrForRow(visibleRowIndex, TR); - - //Render row headers - this.renderRowHeaders(sourceRowIndex, TR); - - this.adjustColumns(TR, displayTds + this.rowHeaderCount); - - lastTD = this.renderCells(sourceRowIndex, TR, displayTds); - - offsetRow = this.instance.getSetting('offsetRow'); //refresh the value - - //after last column is rendered, check if last cell is fully displayed - if (!isWorkingOnClone) { - res = this.wtTable.getRowStrategy().add(visibleRowIndex, lastTD); - - if (res === false) { - break; - } - - if (visibleRowIndex == 0) { //rendering the first row may caused bottom scrollbar to appear, so we need to refresh the window size - this.instance.wtScrollbars.vertical.readWindowSize(); - } - - this.resetOversizedRow(sourceRowIndex); - } - - - if (TR.firstChild) { - var height = this.instance.getSetting('rowHeight', sourceRowIndex); //if I have 2 fixed columns with one-line content and the 3rd column has a multiline content, this is the way to make sure that the overlay will has same row height - if(height) { - TR.firstChild.style.height = height + 'px'; - } - else { - TR.firstChild.style.height = ''; - } - } - - visibleRowIndex++; - - sourceRowIndex = this.rowFilter.visibleToSource(visibleRowIndex); - } -}; - -WalkontableTableRenderer.prototype.resetOversizedRow = function (sourceRow) { - if (this.instance.wtTable.oversizedRows && this.instance.wtTable.oversizedRows[sourceRow]) { - this.instance.wtTable.oversizedRows[sourceRow] = void 0; //void 0 is faster than delete, see http://jsperf.com/delete-vs-undefined-vs-null/16 - } -}; - -WalkontableTableRenderer.prototype.markOversizedRows = function () { - var previousRowHeight - , trInnerHeight - , sourceRowIndex - , currentTr; - - var rowCount = this.instance.wtTable.TBODY.childNodes.length; - while (rowCount) { - rowCount--; - sourceRowIndex = this.instance.wtTable.rowFilter.visibleToSource(rowCount); - previousRowHeight = this.instance.wtSettings.settings.rowHeight(sourceRowIndex); - currentTr = this.instance.wtTable.getTrForRow(sourceRowIndex); - - trInnerHeight = Handsontable.Dom.innerHeight(currentTr) - 1; - - if ((!previousRowHeight && this.instance.wtSettings.settings.defaultRowHeight < trInnerHeight || previousRowHeight < trInnerHeight)) { - if (!this.instance.wtTable.oversizedRows) { - this.instance.wtTable.oversizedRows = {}; - } - this.instance.wtTable.oversizedRows[sourceRowIndex] = trInnerHeight; - } - } - -}; - -WalkontableTableRenderer.prototype.renderCells = function (sourceRowIndex, TR, displayTds) { - var TD, sourceColIndex; - for (var visibleColIndex = 0; visibleColIndex < displayTds; visibleColIndex++) { - sourceColIndex = this.columnFilter.visibleToSource(visibleColIndex); - if (visibleColIndex === 0) { - TD = TR.childNodes[this.columnFilter.sourceColumnToVisibleRowHeadedColumn(sourceColIndex)]; - } - else { - TD = TD.nextSibling; //http://jsperf.com/nextsibling-vs-indexed-childnodes - } - - //If the number of headers has been reduced, we need to replace excess TH with TD - if (TD.nodeName == 'TH') { - TD = this.utils.replaceThWithTd(TD, TR); - } - - TD.className = ''; - TD.removeAttribute('style'); - - this.instance.getSetting('cellRenderer', sourceRowIndex, sourceColIndex, TD); - - } - - return TD; -}; - -WalkontableTableRenderer.prototype.adjustColumnWidths = function (displayTds) { - var cache = this.instance.wtTable.columnWidthCache; - var cacheChanged = false; - var width; - for (var visibleColIndex = 0; visibleColIndex < displayTds; visibleColIndex++) { - if(this.wtTable.isWorkingOnClone()) { - width = this.instance.cloneSource.wtTable.columnWidthCache[visibleColIndex]; - } - else { - width = this.wtTable.getColumnStrategy().getSize(visibleColIndex); - } - if (width !== cache[visibleColIndex]) { - this.COLGROUP.childNodes[visibleColIndex + this.rowHeaderCount].style.width = width + 'px'; - cache[visibleColIndex] = width; - cacheChanged = true; - } - } -}; - -WalkontableTableRenderer.prototype.appendToTbody = function (TR) { - this.TBODY.appendChild(TR); - this.wtTable.tbodyChildrenLength++; -}; - -WalkontableTableRenderer.prototype.getOrCreateTrForRow = function (rowIndex, currentTr) { - var TR; - - if (rowIndex >= this.wtTable.tbodyChildrenLength) { - TR = this.createRow(); - this.appendToTbody(TR); - } else if (rowIndex === 0) { - TR = this.TBODY.firstChild; - } else { - TR = currentTr.nextSibling; //http://jsperf.com/nextsibling-vs-indexed-childnodes - } - - return TR; -}; - -WalkontableTableRenderer.prototype.createRow = function() { - var TR = document.createElement('TR'); - for (var visibleColIndex = 0; visibleColIndex < this.rowHeaderCount; visibleColIndex++) { - TR.appendChild(document.createElement('TH')); - } - - return TR; -}; - -WalkontableTableRenderer.prototype.renderRowHeader = function(row, col, TH){ - this.rowHeaders[col](row, TH); -}; - -WalkontableTableRenderer.prototype.renderRowHeaders = function(row, TR){ - for (var TH = TR.firstChild, visibleColIndex = 0; visibleColIndex < this.rowHeaderCount; visibleColIndex++) { - - //If the number of row headers increased we need to create TH or replace an existing TD node with TH - if (!TH){ - TH = document.createElement('TH'); - TR.appendChild(TH); - } else if (TH.nodeName == 'TD') { - TH = this.utils.replaceTdWithTh(TH, TR); - } - - this.renderRowHeader(row, visibleColIndex, TH); - TH = TH.nextSibling; //http://jsperf.com/nextsibling-vs-indexed-childnodes - } -}; - -WalkontableTableRenderer.prototype.adjustAvailableNodes = function () { - - this.refreshStretching(); //actually it is wrong position because it assumes rowHeader would be always 50px wide (because we measure before it is filled with text). TODO: debug - - //adjust COLGROUP - this.adjustColGroups(); - - //adjust THEAD - this.adjustThead(); - -}; - -WalkontableTableRenderer.prototype.renderColumnHeaders = function () { - if (!this.columnHeaders.length) { - return; - } - - var columnCount = this.getColumnCount(); - - var TR = this.getTrForColumnHeaders(); - - for (var columnIndex = 0; columnIndex < columnCount; columnIndex++) { - if (this.columnHeaders.length) { - this.renderColumnHeader( this.columnFilter.visibleToSource(columnIndex), TR.childNodes[this.rowHeaderCount + columnIndex]); - } - } -}; - -WalkontableTableRenderer.prototype.adjustColGroups = function () { - var columnCount = this.getColumnCount(); - - //adjust COLGROUP - while (this.wtTable.colgroupChildrenLength < columnCount + this.rowHeaderCount) { - this.COLGROUP.appendChild(document.createElement('COL')); - this.wtTable.colgroupChildrenLength++; - } - while (this.wtTable.colgroupChildrenLength > columnCount + this.rowHeaderCount) { - this.COLGROUP.removeChild(this.COLGROUP.lastChild); - this.wtTable.colgroupChildrenLength--; - if(this.wtTable.columnWidthCache) { - this.wtTable.columnWidthCache.splice(-1,1); - } - } -}; - -WalkontableTableRenderer.prototype.adjustThead = function () { - var columnCount = this.getColumnCount(); - var TR = this.THEAD.firstChild; - if (this.columnHeaders.length) { - if (!TR) { - TR = document.createElement('TR'); - this.THEAD.appendChild(TR); - } - - this.theadChildrenLength = TR.childNodes.length; - while (this.theadChildrenLength < columnCount + this.rowHeaderCount) { - TR.appendChild(document.createElement('TH')); - this.theadChildrenLength++; - } - while (this.theadChildrenLength > columnCount + this.rowHeaderCount) { - TR.removeChild(TR.lastChild); - this.theadChildrenLength--; - } - } - else if (TR) { - Handsontable.Dom.empty(TR); - } -}; - -WalkontableTableRenderer.prototype.getTrForColumnHeaders = function () { - var TR = this.THEAD.firstChild; - if (this.rowHeaderCount) { - this.renderRowHeaders(-1, TR); - } - - return TR; -}; - -WalkontableTableRenderer.prototype.renderColumnHeader = function (col, TR) { - return this.columnHeaders[0](col, TR); -}; - -WalkontableTableRenderer.prototype.getColumnCount = function () { - if (this.wtTable.isWorkingOnClone() && (this.instance.cloneOverlay instanceof WalkontableHorizontalScrollbarNative || this.instance.cloneOverlay instanceof WalkontableCornerScrollbarNative)) { - return this.instance.getSetting('fixedColumnsLeft'); - } - else { - return this.wtTable.getColumnStrategy().cellCount; - } -}; - -WalkontableTableRenderer.prototype.renderColGroups = function () { - for (var colIndex = 0; colIndex < this.wtTable.colgroupChildrenLength; colIndex++) { - if (colIndex < this.rowHeaderCount) { - Handsontable.Dom.addClass(this.COLGROUP.childNodes[colIndex], 'rowHeader'); - } - else { - Handsontable.Dom.removeClass(this.COLGROUP.childNodes[colIndex], 'rowHeader'); - } - } -}; - -WalkontableTableRenderer.prototype.adjustColumns = function (TR, desiredCount) { - var count = TR.childNodes.length; - while (count < desiredCount) { - var TD = document.createElement('TD'); - TR.appendChild(TD); - count++; - } - while (count > desiredCount) { - TR.removeChild(TR.lastChild); - count--; - } -}; - -WalkontableTableRenderer.prototype.refreshStretching = function () { - if (this.wtTable.isWorkingOnClone()) { - return; - } - - var instance = this.instance - , stretchH = instance.getSetting('stretchH') - , totalRows = instance.getSetting('totalRows') - , totalColumns = instance.getSetting('totalColumns'); - - var containerWidthFn = function (cacheWidth) { - var viewportWidth = that.instance.wtViewport.getViewportWidth(cacheWidth); - return viewportWidth; - }; - - var that = this; - - var columnWidthFn = function (i) { - var source_c = that.columnFilter.visibleToSource(i); - if (source_c < totalColumns) { - return instance.getSetting('columnWidth', source_c); - } - }; - - var containerHeightFn = function (cacheHeight) { - if (that.instance.cloneOverlay instanceof WalkontableDebugOverlay || instance.wtSettings.settings.renderAllRows) { - return Infinity; - } - else { - return that.instance.wtViewport.getViewportHeight(cacheHeight); - } - }; - - var rowHeightFn = function (i, TD) { - return instance.wtSettings.settings.defaultRowHeight; - }; - - this.wtTable.columnStrategy = new WalkontableColumnStrategy(instance, containerWidthFn, columnWidthFn, stretchH); - this.wtTable.rowStrategy = new WalkontableRowStrategy(instance, containerHeightFn, rowHeightFn); -}; - -/* - Helper functions, which does not have any side effects - */ -WalkontableTableRenderer.utils = {}; - -WalkontableTableRenderer.utils.replaceTdWithTh = function(TD, TR) { - var TH; - TH = document.createElement('TH'); - TR.insertBefore(TH, TD); - TR.removeChild(TD); - - return TH; -}; - -WalkontableTableRenderer.utils.replaceThWithTd = function(TH, TR) { - var TD = document.createElement('TD'); - TR.insertBefore(TD, TH); - TR.removeChild(TH); - - return TD; -}; - - - -function WalkontableViewport(instance) { - this.instance = instance; - this.resetSettings(); - - var that = this; - $(window).on('resize.walkontable.' + this.instance.guid, function () { - that.clientHeight = that.getWorkspaceHeight(); - }); -} - -//used by scrollbar -WalkontableViewport.prototype.getWorkspaceHeight = function (proposedHeight) { - return this.instance.wtScrollbars.vertical.windowSize; -}; - -WalkontableViewport.prototype.getWorkspaceWidth = function (proposedWidth) { - if (this.instance.wtScrollbars.horizontal.scrollHandler === window){ - return Math.min(this.getContainerFillWidth(), document.documentElement.offsetWidth - this.getWorkspaceOffset().left, document.documentElement.offsetWidth); - } - - return this.instance.wtScrollbars.horizontal.windowSize; - -}; - -WalkontableViewport.prototype.getContainerFillWidth = function() { - - if(this.containerWidth) { - return this.containerWidth; - } - - var mainContainer = this.instance.wtTable.holder, - fillWidth, - dummyElement; - - while(mainContainer.parentNode != document.body && mainContainer.parentNode != null && mainContainer.className.indexOf('handsontable') === -1) { - mainContainer = mainContainer.parentNode; - } - - dummyElement = document.createElement("DIV"); - dummyElement.style.width = "100%"; - dummyElement.style.height = "1px"; - mainContainer.appendChild(dummyElement); - fillWidth = dummyElement.offsetWidth; - - this.containerWidth = fillWidth; - - mainContainer.removeChild(dummyElement); - - return fillWidth; -} - -WalkontableViewport.prototype.getWorkspaceOffset = function () { - return Handsontable.Dom.offset(this.instance.wtTable.TABLE); -}; - -WalkontableViewport.prototype.getWorkspaceActualHeight = function () { - return Handsontable.Dom.outerHeight(this.instance.wtTable.TABLE); -}; - -WalkontableViewport.prototype.getWorkspaceActualWidth = function () { - return Handsontable.Dom.outerWidth(this.instance.wtTable.TABLE) || Handsontable.Dom.outerWidth(this.instance.wtTable.TBODY) || Handsontable.Dom.outerWidth(this.instance.wtTable.THEAD); //IE8 reports 0 as
    element corresponding to params row, col - * @param {Number} row - * @param {Number} col - * @public - * @return {Element} - */ - this.getCell = function (row, col) { - return instance.view.getCellAtCoords(new WalkontableCellCoords(row, col)); - }; - - /** - * Returns property name associated with column number - * @param {Number} col - * @public - * @return {String} - */ - this.colToProp = function (col) { - return datamap.colToProp(col); - }; - - /** - * Returns column number associated with property name - * @param {String} prop - * @public - * @return {Number} - */ - this.propToCol = function (prop) { - return datamap.propToCol(prop); - }; - - /** - * Return value at `row`, `col` - * @param {Number} row - * @param {Number} col - * @public - * @return value (mixed data type) - */ - this.getDataAtCell = function (row, col) { - return datamap.get(row, datamap.colToProp(col)); - }; - - /** - * Return value at `row`, `prop` - * @param {Number} row - * @param {String} prop - * @public - * @return value (mixed data type) - */ - this.getDataAtRowProp = function (row, prop) { - return datamap.get(row, prop); - }; - - /** - * Return value at `col`, where `col` is the visible index of the column - * @param {Number} col - * @public - * @return {Array} value (mixed data type) - */ - this.getDataAtCol = function (col) { - var out = []; - return out.concat.apply(out, datamap.getRange(new WalkontableCellCoords(0, col), new WalkontableCellCoords(priv.settings.data.length - 1, col), datamap.DESTINATION_RENDERER)); - }; - - /** - * Return value at `prop` - * @param {String} prop - * @public - * @return {Array} value (mixed data type) - */ - this.getDataAtProp = function (prop) { - var out = []; - return out.concat.apply(out, datamap.getRange(new WalkontableCellCoords(0, datamap.propToCol(prop)), new WalkontableCellCoords(priv.settings.data.length - 1, datamap.propToCol(prop)), datamap.DESTINATION_RENDERER)); - }; - - /** - * Return original source values at 'col' - * @param {Number} col - * @public - * @returns value (mixed data type) - */ - this.getSourceDataAtCol = function (col) { - var out = [], - data = priv.settings.data; - - for (var i = 0; i < data.length; i++) { - out.push(data[i][col]); - } - - return out; - }; - - /** - * Return original source values at 'row' - * @param {Number} row - * @public - * @returns value {mixed data type} - */ - this.getSourceDataAtRow = function (row) { - return priv.settings.data[row]; - }; - - /** - * Return value at `row` - * @param {Number} row - * @public - * @return value (mixed data type) - */ - this.getDataAtRow = function (row) { - var data = datamap.getRange(new WalkontableCellCoords(row, 0), new WalkontableCellCoords(row, this.countCols() - 1), datamap.DESTINATION_RENDERER); - return data[0]; - }; - - /*** - * Remove "key" property object from cell meta data corresponding to params row,col - * @param {Number} row - * @param {Number} col - * @param {String} key - */ - this.removeCellMeta = function(row, col, key) { - var cellMeta = instance.getCellMeta(row, col); - if(cellMeta[key] != undefined){ - delete priv.cellSettings[row][col][key]; - } - }; - - /** - * Set cell meta data object to corresponding params row, col - * @param {Number} row - * @param {Number} col - * @param {Object} prop - */ - this.setCellMetaObject = function (row, col, prop) { - if (typeof prop === 'object') { - for (var i in prop) { - var key = i, - value = prop[i]; - - this.setCellMeta(row, col, key, value); - } - } - }; - - /** - * Sets cell meta data object "key" corresponding to params row, col - * @param {Number} row - * @param {Number} col - * @param {String} key - * @param {String} val - * - */ - this.setCellMeta = function (row, col, key, val) { - if (!priv.cellSettings[row]) { - priv.cellSettings[row] = []; - } - if (!priv.cellSettings[row][col]) { - priv.cellSettings[row][col] = new priv.columnSettings[col](); - } - priv.cellSettings[row][col][key] = val; - Handsontable.hooks.run(instance, 'afterSetCellMeta', row, col, key, val); - }; - - /** - * Returns cell meta data object corresponding to params row, col - * @param {Number} row - * @param {Number} col - * @public - * @return {Object} - */ - this.getCellMeta = function (row, col) { - var prop = datamap.colToProp(col) - , cellProperties; - - row = translateRowIndex(row); - col = translateColIndex(col); - - if (!priv.columnSettings[col]) { - priv.columnSettings[col] = Handsontable.helper.columnFactory(GridSettings, priv.columnsSettingConflicts); - } - - if (!priv.cellSettings[row]) { - priv.cellSettings[row] = []; - } - if (!priv.cellSettings[row][col]) { - priv.cellSettings[row][col] = new priv.columnSettings[col](); - } - - cellProperties = priv.cellSettings[row][col]; //retrieve cellProperties from cache - - cellProperties.row = row; - cellProperties.col = col; - cellProperties.prop = prop; - cellProperties.instance = instance; - - Handsontable.hooks.run(instance, 'beforeGetCellMeta', row, col, cellProperties); - Handsontable.helper.extend(cellProperties, expandType(cellProperties)); //for `type` added in beforeGetCellMeta - - if (cellProperties.cells) { - var settings = cellProperties.cells.call(cellProperties, row, col, prop); - - if (settings) { - Handsontable.helper.extend(cellProperties, settings); - Handsontable.helper.extend(cellProperties, expandType(settings)); //for `type` added in cells - } - } - - Handsontable.hooks.run(instance, 'afterGetCellMeta', row, col, cellProperties); - - return cellProperties; - }; - - /** - * If displayed rows order is different than the order of rows stored in memory (i.e. sorting is applied) - * we need to translate logical (stored) row index to physical (displayed) index. - * @param row - original row index - * @returns {int} translated row index - */ - function translateRowIndex(row){ - return Handsontable.hooks.execute(instance, 'modifyRow', row); - } - - /** - * If displayed columns order is different than the order of columns stored in memory (i.e. column were moved using manualColumnMove plugin) - * we need to translate logical (stored) column index to physical (displayed) index. - * @param col - original column index - * @returns {int} - translated column index - */ - function translateColIndex(col){ - return Handsontable.hooks.execute(instance, 'modifyCol', col); // warning: this must be done after datamap.colToProp - } - - var rendererLookup = Handsontable.helper.cellMethodLookupFactory('renderer'); - this.getCellRenderer = function (row, col) { - var renderer = rendererLookup.call(this, row, col); - return Handsontable.renderers.getRenderer(renderer); - - }; - - this.getCellEditor = Handsontable.helper.cellMethodLookupFactory('editor'); - - this.getCellValidator = Handsontable.helper.cellMethodLookupFactory('validator'); - - - /** - * Validates all cells using their validator functions and calls callback when finished. Does not render the view - * @param callback - */ - this.validateCells = function (callback) { - var waitingForValidator = new ValidatorsQueue(); - waitingForValidator.onQueueEmpty = callback; - - var i = instance.countRows() - 1; - while (i >= 0) { - var j = instance.countCols() - 1; - while (j >= 0) { - waitingForValidator.addValidatorToQueue(); - instance.validateCell(instance.getDataAtCell(i, j), instance.getCellMeta(i, j), function () { - waitingForValidator.removeValidatorFormQueue(); - }, 'validateCells'); - j--; - } - i--; - } - waitingForValidator.checkIfQueueIsEmpty(); - }; - - /** - * Return array of row headers (if they are enabled). If param `row` given, return header at given row as string - * @param {Number} row (Optional) - * @return {Array|String} - */ - this.getRowHeader = function (row) { - if (row === void 0) { - var out = []; - for (var i = 0, ilen = instance.countRows(); i < ilen; i++) { - out.push(instance.getRowHeader(i)); - } - return out; - } - else if (Object.prototype.toString.call(priv.settings.rowHeaders) === '[object Array]' && priv.settings.rowHeaders[row] !== void 0) { - return priv.settings.rowHeaders[row]; - } - else if (typeof priv.settings.rowHeaders === 'function') { - return priv.settings.rowHeaders(row); - } - else if (priv.settings.rowHeaders && typeof priv.settings.rowHeaders !== 'string' && typeof priv.settings.rowHeaders !== 'number') { - return row + 1; - } - else { - return priv.settings.rowHeaders; - } - }; - - /** - * Returns information of this table is configured to display row headers - * @returns {boolean} - */ - this.hasRowHeaders = function () { - return !!priv.settings.rowHeaders; - }; - - /** - * Returns information of this table is configured to display column headers - * @returns {boolean} - */ - this.hasColHeaders = function () { - if (priv.settings.colHeaders !== void 0 && priv.settings.colHeaders !== null) { //Polymer has empty value = null - return !!priv.settings.colHeaders; - } - for (var i = 0, ilen = instance.countCols(); i < ilen; i++) { - if (instance.getColHeader(i)) { - return true; - } - } - return false; - }; - - /** - * Return array of column headers (if they are enabled). If param `col` given, return header at given column as string - * @param {Number} col (Optional) - * @return {Array|String} - */ - this.getColHeader = function (col) { - if (col === void 0) { - var out = []; - for (var i = 0, ilen = instance.countCols(); i < ilen; i++) { - out.push(instance.getColHeader(i)); - } - return out; - } - else { - var baseCol = col; - col = Handsontable.hooks.execute(instance, 'modifyCol', col); - - if (priv.settings.columns && priv.settings.columns[col] && priv.settings.columns[col].title) { - return priv.settings.columns[col].title; - } - else if (Object.prototype.toString.call(priv.settings.colHeaders) === '[object Array]' && priv.settings.colHeaders[col] !== void 0) { - return priv.settings.colHeaders[col]; - } - else if (typeof priv.settings.colHeaders === 'function') { - return priv.settings.colHeaders(col); - } - else if (priv.settings.colHeaders && typeof priv.settings.colHeaders !== 'string' && typeof priv.settings.colHeaders !== 'number') { - return Handsontable.helper.spreadsheetColumnLabel(baseCol); //see #1458 - } - else { - return priv.settings.colHeaders; - } - } - }; - - /** - * Return column width from settings (no guessing). Private use intended - * @param {Number} col - * @return {Number} - */ - this._getColWidthFromSettings = function (col) { - var cellProperties = instance.getCellMeta(0, col); - var width = cellProperties.width; - if (width === void 0 || width === priv.settings.width) { - width = cellProperties.colWidths; - } - if (width !== void 0 && width !== null) { - switch (typeof width) { - case 'object': //array - width = width[col]; - break; - - case 'function': - width = width(col); - break; - } - if (typeof width === 'string') { - width = parseInt(width, 10); - } - } - return width; - }; - - /** - * Return column width - * @param {Number} col - * @return {Number} - */ - this.getColWidth = function (col) { - var width = instance._getColWidthFromSettings(col); - if (!width) { - width = 50; - } - width = Handsontable.hooks.execute(instance, 'modifyColWidth', width, col); - return width; - }; - - /** - * Return row height from settings (no guessing). Private use intended - * @param {Number} row - * @return {Number} - */ - this._getRowHeightFromSettings= function (row) { - /* inefficient - var cellProperties = instance.getCellMeta(0, row); - var height = cellProperties.height; - if (height === void 0 || height === priv.settings.height) { - height = cellProperties.rowHeights; - } - */ - var height = priv.settings.rowHeights; //only uses grid settings - if (height !== void 0 && height !== null) { - switch (typeof height) { - case 'object': //array - height = height[row]; - break; - - case 'function': - height = height(row); - break; - } - if (typeof height === 'string') { - height = parseInt(height, 10); - } - } - return height; - }; - - /** - * Return row height - * @param {Number} row - * @return {Number} - */ - this.getRowHeight = function (row) { - var height = instance._getRowHeightFromSettings(row), - oversizedHeight = instance.checkIfRowIsOversized(row); - - height = Handsontable.hooks.execute(instance, 'modifyRowHeight', height, row); - - if(oversizedHeight) { - height = height ? Math.max(height,oversizedHeight) : oversizedHeight; - } - - return height; - }; - - - - /** - * Checks if any of the row's cells content exceeds its initial height, and if so, returns the oversized height - * @param {Number} row - * @return {Number} - */ - this.checkIfRowIsOversized = function(row) { - if(instance.view.wt.wtTable.oversizedRows) { - return instance.view.wt.wtTable.oversizedRows[row]; - } - }; - - - /** - * Return total number of rows in grid - * @return {Number} - */ - this.countRows = function () { - return priv.settings.data.length; - }; - - /** - * Return total number of columns in grid - * @return {Number} - */ - this.countCols = function () { - if (instance.dataType === 'object' || instance.dataType === 'function') { - if (priv.settings.columns && priv.settings.columns.length) { - return priv.settings.columns.length; - } - else { - return datamap.colToPropCache.length; - } - } - else if (instance.dataType === 'array') { - if (priv.settings.columns && priv.settings.columns.length) { - return priv.settings.columns.length; - } - else if (priv.settings.data && priv.settings.data[0] && priv.settings.data[0].length) { - return priv.settings.data[0].length; - } - else { - return 0; - } - } - }; - - /** - * Return index of first visible row - * @return {Number} - */ - this.rowOffset = function () { - return instance.view.wt.getSetting('offsetRow'); //actually offsetRow is the first rendered row, not neccessarily first visible - }; - - /** - * Return index of first visible column - * @return {Number} - */ - this.colOffset = function () { - return 0; //all columns are always rendered - }; - - /** - * Return number of visible rows. Returns -1 if table is not visible - * @return {Number} - */ - this.countVisibleRows = function () { - return instance.view.wt.drawn ? instance.view.wt.wtTable.rowStrategy.countVisible() : -1; - }; - - /** - * Return number of visible columns. Returns -1 if table is not visible - * @return {Number} - */ - this.countVisibleCols = function () { - return instance.view.wt.drawn ? instance.view.wt.wtTable.columnStrategy.countVisible() : -1; - }; - - /** - * Return number of empty rows - * @return {Boolean} ending If true, will only count empty rows at the end of the data source - */ - this.countEmptyRows = function (ending) { - var i = instance.countRows() - 1 - , empty = 0 - , row; - while (i >= 0) { - row = Handsontable.hooks.execute(this, 'modifyRow', i); - if (instance.isEmptyRow(row)) { - empty++; - } - else if (ending) { - break; - } - i--; - } - return empty; - }; - - /** - * Return number of empty columns - * @return {Boolean} ending If true, will only count empty columns at the end of the data source row - */ - this.countEmptyCols = function (ending) { - if (instance.countRows() < 1) { - return 0; - } - - var i = instance.countCols() - 1 - , empty = 0; - while (i >= 0) { - if (instance.isEmptyCol(i)) { - empty++; - } - else if (ending) { - break; - } - i--; - } - return empty; - }; - - /** - * Return true if the row at the given index is empty, false otherwise - * @param {Number} r Row index - * @return {Boolean} - */ - this.isEmptyRow = function (r) { - return priv.settings.isEmptyRow.call(instance, r); - }; - - /** - * Return true if the column at the given index is empty, false otherwise - * @param {Number} c Column index - * @return {Boolean} - */ - this.isEmptyCol = function (c) { - return priv.settings.isEmptyCol.call(instance, c); - }; - - /** - * Selects cell on grid. Optionally selects range to another cell - * @param {Number} row - * @param {Number} col - * @param {Number} [endRow] - * @param {Number} [endCol] - * @param {Boolean} [scrollToCell=true] If true, viewport will be scrolled to the selection - * @public - * @return {Boolean} - */ - this.selectCell = function (row, col, endRow, endCol, scrollToCell) { - if (typeof row !== 'number' || row < 0 || row >= instance.countRows()) { - return false; - } - if (typeof col !== 'number' || col < 0 || col >= instance.countCols()) { - return false; - } - if (typeof endRow !== "undefined") { - if (typeof endRow !== 'number' || endRow < 0 || endRow >= instance.countRows()) { - return false; - } - if (typeof endCol !== 'number' || endCol < 0 || endCol >= instance.countCols()) { - return false; - } - } - var coords = new WalkontableCellCoords(row, col); - priv.selRange = new WalkontableCellRange(coords, coords, coords); - if (document.activeElement && document.activeElement !== document.documentElement && document.activeElement !== document.body) { - document.activeElement.blur(); //needed or otherwise prepare won't focus the cell. selectionSpec tests this (should move focus to selected cell) - } - instance.listen(); - if (typeof endRow === "undefined") { - selection.setRangeEnd(priv.selRange.from, scrollToCell); - } - else { - selection.setRangeEnd(new WalkontableCellCoords(endRow, endCol), scrollToCell); - } - - instance.selection.finish(); - return true; - }; - - this.selectCellByProp = function (row, prop, endRow, endProp, scrollToCell) { - arguments[1] = datamap.propToCol(arguments[1]); - if (typeof arguments[3] !== "undefined") { - arguments[3] = datamap.propToCol(arguments[3]); - } - return instance.selectCell.apply(instance, arguments); - }; - - /** - * Deselects current sell selection on grid - * @public - */ - this.deselectCell = function () { - selection.deselect(); - }; - - /** - * Remove grid from DOM - * @public - */ - this.destroy = function () { - instance._clearTimeouts(); - if (instance.view) { //in case HT is destroyed before initialization has finished - instance.view.wt.destroy(); - } - instance.rootElement.empty(); - instance.rootElement.removeData('handsontable'); - instance.rootElement.off('.handsontable'); - $(window).off('.' + instance.guid); - $document.off('.' + instance.guid); - $body.off('.' + instance.guid); - Handsontable.hooks.run(instance, 'afterDestroy'); - Handsontable.hooks.destroy(instance); - - for (var i in instance) { - if (instance.hasOwnProperty(i)) { - //replace instance methods with post mortem - if (typeof instance[i] === "function") { - if (i !== "runHooks" && i !== "runHooksAndReturn") { - instance[i] = postMortem; - } - } - //replace instance properties with null (restores memory) - //it should not be necessary but this prevents a memory leak side effects that show itself in Jasmine tests - else if (i !== "guid") { - instance[i] = null; - } - } - } - - //replace private properties with null (restores memory) - //it should not be necessary but this prevents a memory leak side effects that show itself in Jasmine tests - priv = null; - datamap = null; - grid = null; - selection = null; - editorManager = null; - instance = null; - GridSettings = null; - $document = null; - $body = null; - }; - - /** - * Replacement for all methods after Handsotnable was destroyed - */ - function postMortem() { - throw new Error("This method cannot be called because this Handsontable instance has been destroyed"); - }; - - /** - * Returns active editor object - * @returns {Object} - */ - this.getActiveEditor = function(){ - return editorManager.getActiveEditor(); - }; - - /** - * Return Handsontable instance - * @public - * @return {Object} - */ - this.getInstance = function () { - return instance; - }; - - this.addHook = function (key, fn) { - Handsontable.hooks.add(key, fn, instance); - }; - - this.addHookOnce = function (key, fn) { - Handsontable.hooks.once(key, fn, instance); - }; - - this.removeHook = function (key, fn) { - Handsontable.hooks.remove(key, fn, instance); - }; - - this.runHooks = function (key, p1, p2, p3, p4, p5, p6) { - Handsontable.hooks.run(instance, key, p1, p2, p3, p4, p5, p6); - }; - - this.runHooksAndReturn = function (key, p1, p2, p3, p4, p5, p6) { - return Handsontable.hooks.execute(instance, key, p1, p2, p3, p4, p5, p6); - }; - - this.timeouts = []; - - /** - * Sets timeout. Purpose of this method is to clear all known timeouts when `destroy` method is called - * @public - */ - this._registerTimeout = function (handle) { - this.timeouts.push(handle); - }; - - /** - * Clears all known timeouts - * @public - */ - this._clearTimeouts = function () { - for(var i = 0, ilen = this.timeouts.length; i 1) { - for (i = 1, ilen = arguments.length; i < ilen; i++) { - args.push(arguments[i]); - } - } - - if (instance) { - if (typeof instance[action] !== 'undefined') { - output = instance[action].apply(instance, args); - } - else { - throw new Error('Handsontable do not provide action: ' + action); - } - } - - return output; - } -}; - -(function (window) { - 'use strict'; - - function MultiMap() { - var map = { - arrayMap: [], - weakMap: new WeakMap() - }; - - return { - 'get': function (key) { - if (canBeAnArrayMapKey(key)) { - return map.arrayMap[key]; - } else if (canBeAWeakMapKey(key)) { - return map.weakMap.get(key); - } - }, - - 'set': function (key, value) { - if (canBeAnArrayMapKey(key)) { - map.arrayMap[key] = value; - } else if (canBeAWeakMapKey(key)) { - map.weakMap.set(key, value); - } else { - throw new Error('Invalid key type'); - } - - - }, - - 'delete': function (key) { - if (canBeAnArrayMapKey(key)) { - delete map.arrayMap[key]; - } else if (canBeAWeakMapKey(key)) { - map.weakMap['delete'](key); //Delete must be called using square bracket notation, because IE8 does not handle using `delete` with dot notation - } - } - }; - - - - function canBeAnArrayMapKey(obj){ - return obj !== null && !isNaNSymbol(obj) && (typeof obj == 'string' || typeof obj == 'number'); - } - - function canBeAWeakMapKey(obj){ - return obj !== null && (typeof obj == 'object' || typeof obj == 'function'); - } - - function isNaNSymbol(obj){ - return obj !== obj; // NaN === NaN is always false - } - - } - - if (!window.MultiMap){ - window.MultiMap = MultiMap; - } - -})(window); -/** - * DOM helper optimized for maximum performance - * It is recommended for Handsontable plugins and renderers, because it is much faster than jQuery - * @type {Object} - */ -if(!window.Handsontable) { - var Handsontable = {}; //required because Walkontable test suite uses this class directly -} -Handsontable.Dom = {}; - -//goes up the DOM tree (including given element) until it finds an element that matches the nodeName -Handsontable.Dom.closest = function (elem, nodeNames, until) { - while (elem != null && elem !== until) { - if (elem.nodeType === 1 && nodeNames.indexOf(elem.nodeName) > -1) { - return elem; - } - elem = elem.parentNode; - } - return null; -}; - -//goes up the DOM tree and checks if element is child of another element -Handsontable.Dom.isChildOf = function (child, parent) { - var node = child.parentNode; - while (node != null) { - if (node == parent) { - return true; - } - node = node.parentNode; - } - return false; -}; - -/** - * Counts index of element within its parent - * WARNING: for performance reasons, assumes there are only element nodes (no text nodes). This is true for Walkotnable - * Otherwise would need to check for nodeType or use previousElementSibling - * @see http://jsperf.com/sibling-index/10 - * @param {Element} elem - * @return {Number} - */ -Handsontable.Dom.index = function (elem) { - var i = 0; - while (elem = elem.previousSibling) { - ++i - } - return i; -}; - -if (document.documentElement.classList) { - // HTML5 classList API - Handsontable.Dom.hasClass = function (ele, cls) { - return ele.classList.contains(cls); - }; - - Handsontable.Dom.addClass = function (ele, cls) { - ele.classList.add(cls); - }; - - Handsontable.Dom.removeClass = function (ele, cls) { - ele.classList.remove(cls); - }; -} -else { - //http://snipplr.com/view/3561/addclass-removeclass-hasclass/ - Handsontable.Dom.hasClass = function (ele, cls) { - return ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)')); - }; - - Handsontable.Dom.addClass = function (ele, cls) { - if(ele.className == "") ele.className = cls; - else if (!this.hasClass(ele, cls)) ele.className += " " + cls; - }; - - Handsontable.Dom.removeClass = function (ele, cls) { - if (this.hasClass(ele, cls)) { //is this really needed? - var reg = new RegExp('(\\s|^)' + cls + '(\\s|$)'); - ele.className = ele.className.replace(reg, ' ').trim(); //String.prototype.trim is defined in polyfill.js - } - }; -} - -/*//http://net.tutsplus.com/tutorials/javascript-ajax/javascript-from-null-cross-browser-event-binding/ - Handsontable.Dom.addEvent = (function () { - var that = this; - if (document.addEventListener) { - return function (elem, type, cb) { - if ((elem && !elem.length) || elem === window) { - elem.addEventListener(type, cb, false); - } - else if (elem && elem.length) { - var len = elem.length; - for (var i = 0; i < len; i++) { - that.addEvent(elem[i], type, cb); - } - } - }; - } - else { - return function (elem, type, cb) { - if ((elem && !elem.length) || elem === window) { - elem.attachEvent('on' + type, function () { - - //normalize - //http://stackoverflow.com/questions/4643249/cross-browser-event-object-normalization - var e = window['event']; - e.target = e.srcElement; - //e.offsetX = e.layerX; - //e.offsetY = e.layerY; - e.relatedTarget = e.relatedTarget || e.type == 'mouseover' ? e.fromElement : e.toElement; - if (e.target.nodeType === 3) e.target = e.target.parentNode; //Safari bug - - return cb.call(elem, e) - }); - } - else if (elem.length) { - var len = elem.length; - for (var i = 0; i < len; i++) { - that.addEvent(elem[i], type, cb); - } - } - }; - } - })(); - - Handsontable.Dom.triggerEvent = function (element, eventName, target) { - var event; - if (document.createEvent) { - event = document.createEvent("MouseEvents"); - event.initEvent(eventName, true, true); - } else { - event = document.createEventObject(); - event.eventType = eventName; - } - - event.eventName = eventName; - event.target = target; - - if (document.createEvent) { - target.dispatchEvent(event); - } else { - target.fireEvent("on" + event.eventType, event); - } - };*/ - -Handsontable.Dom.removeTextNodes = function (elem, parent) { - if (elem.nodeType === 3) { - parent.removeChild(elem); //bye text nodes! - } - else if (['TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR'].indexOf(elem.nodeName) > -1) { - var childs = elem.childNodes; - for (var i = childs.length - 1; i >= 0; i--) { - this.removeTextNodes(childs[i], elem); - } - } -}; - -/** - * Remove childs function - * WARNING - this doesn't unload events and data attached by jQuery - * http://jsperf.com/jquery-html-vs-empty-vs-innerhtml/9 - * http://jsperf.com/jquery-html-vs-empty-vs-innerhtml/11 - no siginificant improvement with Chrome remove() method - * @param element - * @returns {void} - */ -// -Handsontable.Dom.empty = function (element) { - var child; - while (child = element.lastChild) { - element.removeChild(child); - } -}; - -Handsontable.Dom.HTML_CHARACTERS = /(<(.*)>|&(.*);)/; - -/** - * Insert content into element trying avoid innerHTML method. - * @return {void} - */ -Handsontable.Dom.fastInnerHTML = function (element, content) { - if (this.HTML_CHARACTERS.test(content)) { - element.innerHTML = content; - } - else { - this.fastInnerText(element, content); - } -}; - -/** - * Insert text content into element - * @return {void} - */ -if (document.createTextNode('test').textContent) { //STANDARDS - Handsontable.Dom.fastInnerText = function (element, content) { - var child = element.firstChild; - if (child && child.nodeType === 3 && child.nextSibling === null) { - //fast lane - replace existing text node - //http://jsperf.com/replace-text-vs-reuse - child.textContent = content; - } - else { - //slow lane - empty element and insert a text node - this.empty(element); - element.appendChild(document.createTextNode(content)); - } - }; -} -else { //IE8 - Handsontable.Dom.fastInnerText = function (element, content) { - var child = element.firstChild; - if (child && child.nodeType === 3 && child.nextSibling === null) { - //fast lane - replace existing text node - //http://jsperf.com/replace-text-vs-reuse - child.data = content; - } - else { - //slow lane - empty element and insert a text node - this.empty(element); - element.appendChild(document.createTextNode(content)); - } - }; -} - -/** - * Returns true/false depending if element has offset parent - * @param elem - * @returns {boolean} - */ -/*if (document.createTextNode('test').textContent) { //STANDARDS - Handsontable.Dom.hasOffsetParent = function (elem) { - return !!elem.offsetParent; - } -} -else { - Handsontable.Dom.hasOffsetParent = function (elem) { - try { - if (!elem.offsetParent) { - return false; - } - } - catch (e) { - return false; //IE8 throws "Unspecified error" when offsetParent is not found - we catch it here - } - return true; - } -}*/ - -/** - * Returns true if element is attached to the DOM and visible, false otherwise - * @param elem - * @returns {boolean} - */ -Handsontable.Dom.isVisible = function (elem) { - //fast method according to benchmarks, but requires layout so slow in our case - /* - if (!Handsontable.Dom.hasOffsetParent(elem)) { - return false; //fixes problem with UI Bootstrap directive - } - -// if (elem.offsetWidth > 0 || (elem.parentNode && elem.parentNode.offsetWidth > 0)) { //IE10 was mistaken here - if (elem.offsetWidth > 0) { - return true; - } - */ - - //slow method - var next = elem; - while (next !== document.documentElement) { //until reached - if (next === null) { //parent detached from DOM - return false; - } - else if (next.nodeType === 11) { //nodeType == 1 -> DOCUMENT_FRAGMENT_NODE - if (next.host) { //this is Web Components Shadow DOM - //see: http://w3c.github.io/webcomponents/spec/shadow/#encapsulation - //according to spec, should be if (next.ownerDocument !== window.document), but that doesn't work yet - if (next.host.impl) { //Chrome 33.0.1723.0 canary (2013-11-29) Web Platform features disabled - return Handsontable.Dom.isVisible(next.host.impl); - } - else if (next.host) { //Chrome 33.0.1723.0 canary (2013-11-29) Web Platform features enabled - return Handsontable.Dom.isVisible(next.host); - } - else { - throw new Error("Lost in Web Components world"); - } - } - else { - return false; //this is a node detached from document in IE8 - } - } - else if (next.style.display === 'none') { - return false; - } - next = next.parentNode; - } - return true; -}; - -/** - * Returns elements top and left offset relative to the document. In our usage case compatible with jQuery but 2x faster - * @param {HTMLElement} elem - * @return {Object} - */ -Handsontable.Dom.offset = function (elem) { - if (this.hasCaptionProblem() && elem.firstChild && elem.firstChild.nodeName === 'CAPTION') { - //fixes problem with Firefox ignoring
    in TABLE offset (see also Handsontable.Dom.outerHeight) - //http://jsperf.com/offset-vs-getboundingclientrect/8 - var box = elem.getBoundingClientRect(); - return { - top: box.top + (window.pageYOffset || document.documentElement.scrollTop) - (document.documentElement.clientTop || 0), - left: box.left + (window.pageXOffset || document.documentElement.scrollLeft) - (document.documentElement.clientLeft || 0) - }; - } - - var offsetLeft = elem.offsetLeft - , offsetTop = elem.offsetTop - , lastElem = elem; - - while (elem = elem.offsetParent) { - if (elem === document.body) { //from my observation, document.body always has scrollLeft/scrollTop == 0 - break; - } - offsetLeft += elem.offsetLeft; - offsetTop += elem.offsetTop; - lastElem = elem; - } - - if (lastElem && lastElem.style.position === 'fixed') { //slow - http://jsperf.com/offset-vs-getboundingclientrect/6 - //if(lastElem !== document.body) { //faster but does gives false positive in Firefox - offsetLeft += window.pageXOffset || document.documentElement.scrollLeft; - offsetTop += window.pageYOffset || document.documentElement.scrollTop; - } - - return { - left: offsetLeft, - top: offsetTop - }; -}; - -Handsontable.Dom.getWindowScrollTop = function () { - var res = window.scrollY; - if (res == void 0) { //IE8-11 - res = document.documentElement.scrollTop; - } - return res; -}; - -Handsontable.Dom.getWindowScrollLeft = function () { - var res = window.scrollX; - if (res == void 0) { //IE8-11 - res = document.documentElement.scrollLeft; - } - return res; -}; - -Handsontable.Dom.getScrollTop = function (elem) { - if (elem === window) { - return Handsontable.Dom.getWindowScrollTop(elem); - } - else { - return elem.scrollTop; - } -}; - -Handsontable.Dom.getScrollLeft = function (elem) { - if (elem === window) { - return Handsontable.Dom.getWindowScrollLeft(elem); - } - else { - return elem.scrollLeft; - } -}; - -Handsontable.Dom.getComputedStyle = function (elem) { - return elem.currentStyle || document.defaultView.getComputedStyle(elem); -}; - -Handsontable.Dom.outerWidth = function (elem) { - return elem.offsetWidth; -}; - -Handsontable.Dom.outerHeight = function (elem) { - if (this.hasCaptionProblem() && elem.firstChild && elem.firstChild.nodeName === 'CAPTION') { - //fixes problem with Firefox ignoring in TABLE.offsetHeight - //jQuery (1.10.1) still has this unsolved - //may be better to just switch to getBoundingClientRect - //http://bililite.com/blog/2009/03/27/finding-the-size-of-a-table/ - //http://lists.w3.org/Archives/Public/www-style/2009Oct/0089.html - //http://bugs.jquery.com/ticket/2196 - //http://lists.w3.org/Archives/Public/www-style/2009Oct/0140.html#start140 - return elem.offsetHeight + elem.firstChild.offsetHeight; - } - else { - return elem.offsetHeight; - } -}; - -Handsontable.Dom.innerHeight = function (elem) { - return elem.clientHeight; -}; - -Handsontable.Dom.innerWidth = function (elem) { - return elem.innerWidth; -}; - -(function () { - var hasCaptionProblem; - - function detectCaptionProblem() { - var TABLE = document.createElement('TABLE'); - TABLE.style.borderSpacing = 0; - TABLE.style.borderWidth = 0; - TABLE.style.padding = 0; - var TBODY = document.createElement('TBODY'); - TABLE.appendChild(TBODY); - TBODY.appendChild(document.createElement('TR')); - TBODY.firstChild.appendChild(document.createElement('TD')); - TBODY.firstChild.firstChild.innerHTML = '
    t
    t
    offsetWidth; -}; - -WalkontableViewport.prototype.getColumnHeaderHeight = function () { - if (isNaN(this.columnHeaderHeight)) { - var cellOffset = Handsontable.Dom.offset(this.instance.wtTable.TBODY) - , tableOffset = this.instance.wtTable.tableOffset; - this.columnHeaderHeight = cellOffset.top - tableOffset.top; - } - return this.columnHeaderHeight; -}; - -WalkontableViewport.prototype.getViewportHeight = function (proposedHeight) { - - var containerHeight = this.getWorkspaceHeight(proposedHeight); - - if (containerHeight === Infinity) { - return containerHeight; - } - - var columnHeaderHeight = this.getColumnHeaderHeight(); - if (columnHeaderHeight > 0) { - containerHeight -= columnHeaderHeight; - } - - return containerHeight; - -}; - -WalkontableViewport.prototype.getRowHeaderWidth = function () { - if (this.instance.cloneSource) { - return this.instance.cloneSource.wtViewport.getRowHeaderWidth(); - } - if (isNaN(this.rowHeaderWidth)) { - var rowHeaders = this.instance.getSetting('rowHeaders'); - if (rowHeaders.length) { - var TH = this.instance.wtTable.TABLE.querySelector('TH'); - this.rowHeaderWidth = 0; - for (var i = 0, ilen = rowHeaders.length; i < ilen; i++) { - if (TH) { - this.rowHeaderWidth += Handsontable.Dom.outerWidth(TH); - TH = TH.nextSibling; - } - else { - this.rowHeaderWidth += 50; //yes this is a cheat but it worked like that before, just taking assumption from CSS instead of measuring. TODO: proper fix - } - } - } - else { - this.rowHeaderWidth = 0; - } - } - return this.rowHeaderWidth; -}; - -// Viewport width = Workspace width - Row Headers width -WalkontableViewport.prototype.getViewportWidth = function (proposedWidth) { - var containerWidth = this.getWorkspaceWidth(proposedWidth); - - if (containerWidth === Infinity) { - return containerWidth; - } - - var rowHeaderWidth = this.getRowHeaderWidth(); - if (rowHeaderWidth > 0) { - return containerWidth - rowHeaderWidth; - } - else { - return containerWidth; - } -}; - -WalkontableViewport.prototype.resetSettings = function () { - this.rowHeaderWidth = NaN; - this.columnHeaderHeight = NaN; -}; -})(jQuery, window, Handsontable); -/*! - * numeral.js - * version : 1.5.3 - * author : Adam Draper - * license : MIT - * http://adamwdraper.github.com/Numeral-js/ - */ - -(function () { - - /************************************ - Constants - ************************************/ - - var numeral, - VERSION = '1.5.3', - // internal storage for language config files - languages = {}, - currentLanguage = 'en', - zeroFormat = null, - defaultFormat = '0,0', - // check for nodeJS - hasModule = (typeof module !== 'undefined' && module.exports); - - - /************************************ - Constructors - ************************************/ - - - // Numeral prototype object - function Numeral (number) { - this._value = number; - } - - /** - * Implementation of toFixed() that treats floats more like decimals - * - * Fixes binary rounding issues (eg. (0.615).toFixed(2) === '0.61') that present - * problems for accounting- and finance-related software. - */ - function toFixed (value, precision, roundingFunction, optionals) { - var power = Math.pow(10, precision), - optionalsRegExp, - output; - - //roundingFunction = (roundingFunction !== undefined ? roundingFunction : Math.round); - // Multiply up by precision, round accurately, then divide and use native toFixed(): - output = (roundingFunction(value * power) / power).toFixed(precision); - - if (optionals) { - optionalsRegExp = new RegExp('0{1,' + optionals + '}$'); - output = output.replace(optionalsRegExp, ''); - } - - return output; - } - - /************************************ - Formatting - ************************************/ - - // determine what type of formatting we need to do - function formatNumeral (n, format, roundingFunction) { - var output; - - // figure out what kind of format we are dealing with - if (format.indexOf('$') > -1) { // currency!!!!! - output = formatCurrency(n, format, roundingFunction); - } else if (format.indexOf('%') > -1) { // percentage - output = formatPercentage(n, format, roundingFunction); - } else if (format.indexOf(':') > -1) { // time - output = formatTime(n, format); - } else { // plain ol' numbers or bytes - output = formatNumber(n._value, format, roundingFunction); - } - - // return string - return output; - } - - // revert to number - function unformatNumeral (n, string) { - var stringOriginal = string, - thousandRegExp, - millionRegExp, - billionRegExp, - trillionRegExp, - suffixes = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], - bytesMultiplier = false, - power; - - if (string.indexOf(':') > -1) { - n._value = unformatTime(string); - } else { - if (string === zeroFormat) { - n._value = 0; - } else { - if (languages[currentLanguage].delimiters.decimal !== '.') { - string = string.replace(/\./g,'').replace(languages[currentLanguage].delimiters.decimal, '.'); - } - - // see if abbreviations are there so that we can multiply to the correct number - thousandRegExp = new RegExp('[^a-zA-Z]' + languages[currentLanguage].abbreviations.thousand + '(?:\\)|(\\' + languages[currentLanguage].currency.symbol + ')?(?:\\))?)?$'); - millionRegExp = new RegExp('[^a-zA-Z]' + languages[currentLanguage].abbreviations.million + '(?:\\)|(\\' + languages[currentLanguage].currency.symbol + ')?(?:\\))?)?$'); - billionRegExp = new RegExp('[^a-zA-Z]' + languages[currentLanguage].abbreviations.billion + '(?:\\)|(\\' + languages[currentLanguage].currency.symbol + ')?(?:\\))?)?$'); - trillionRegExp = new RegExp('[^a-zA-Z]' + languages[currentLanguage].abbreviations.trillion + '(?:\\)|(\\' + languages[currentLanguage].currency.symbol + ')?(?:\\))?)?$'); - - // see if bytes are there so that we can multiply to the correct number - for (power = 0; power <= suffixes.length; power++) { - bytesMultiplier = (string.indexOf(suffixes[power]) > -1) ? Math.pow(1024, power + 1) : false; - - if (bytesMultiplier) { - break; - } - } - - // do some math to create our number - n._value = ((bytesMultiplier) ? bytesMultiplier : 1) * ((stringOriginal.match(thousandRegExp)) ? Math.pow(10, 3) : 1) * ((stringOriginal.match(millionRegExp)) ? Math.pow(10, 6) : 1) * ((stringOriginal.match(billionRegExp)) ? Math.pow(10, 9) : 1) * ((stringOriginal.match(trillionRegExp)) ? Math.pow(10, 12) : 1) * ((string.indexOf('%') > -1) ? 0.01 : 1) * (((string.split('-').length + Math.min(string.split('(').length-1, string.split(')').length-1)) % 2)? 1: -1) * Number(string.replace(/[^0-9\.]+/g, '')); - - // round if we are talking about bytes - n._value = (bytesMultiplier) ? Math.ceil(n._value) : n._value; - } - } - return n._value; - } - - function formatCurrency (n, format, roundingFunction) { - var symbolIndex = format.indexOf('$'), - openParenIndex = format.indexOf('('), - minusSignIndex = format.indexOf('-'), - space = '', - spliceIndex, - output; - - // check for space before or after currency - if (format.indexOf(' $') > -1) { - space = ' '; - format = format.replace(' $', ''); - } else if (format.indexOf('$ ') > -1) { - space = ' '; - format = format.replace('$ ', ''); - } else { - format = format.replace('$', ''); - } - - // format the number - output = formatNumber(n._value, format, roundingFunction); - - // position the symbol - if (symbolIndex <= 1) { - if (output.indexOf('(') > -1 || output.indexOf('-') > -1) { - output = output.split(''); - spliceIndex = 1; - if (symbolIndex < openParenIndex || symbolIndex < minusSignIndex){ - // the symbol appears before the "(" or "-" - spliceIndex = 0; - } - output.splice(spliceIndex, 0, languages[currentLanguage].currency.symbol + space); - output = output.join(''); - } else { - output = languages[currentLanguage].currency.symbol + space + output; - } - } else { - if (output.indexOf(')') > -1) { - output = output.split(''); - output.splice(-1, 0, space + languages[currentLanguage].currency.symbol); - output = output.join(''); - } else { - output = output + space + languages[currentLanguage].currency.symbol; - } - } - - return output; - } - - function formatPercentage (n, format, roundingFunction) { - var space = '', - output, - value = n._value * 100; - - // check for space before % - if (format.indexOf(' %') > -1) { - space = ' '; - format = format.replace(' %', ''); - } else { - format = format.replace('%', ''); - } - - output = formatNumber(value, format, roundingFunction); - - if (output.indexOf(')') > -1 ) { - output = output.split(''); - output.splice(-1, 0, space + '%'); - output = output.join(''); - } else { - output = output + space + '%'; - } - - return output; - } - - function formatTime (n) { - var hours = Math.floor(n._value/60/60), - minutes = Math.floor((n._value - (hours * 60 * 60))/60), - seconds = Math.round(n._value - (hours * 60 * 60) - (minutes * 60)); - return hours + ':' + ((minutes < 10) ? '0' + minutes : minutes) + ':' + ((seconds < 10) ? '0' + seconds : seconds); - } - - function unformatTime (string) { - var timeArray = string.split(':'), - seconds = 0; - // turn hours and minutes into seconds and add them all up - if (timeArray.length === 3) { - // hours - seconds = seconds + (Number(timeArray[0]) * 60 * 60); - // minutes - seconds = seconds + (Number(timeArray[1]) * 60); - // seconds - seconds = seconds + Number(timeArray[2]); - } else if (timeArray.length === 2) { - // minutes - seconds = seconds + (Number(timeArray[0]) * 60); - // seconds - seconds = seconds + Number(timeArray[1]); - } - return Number(seconds); - } - - function formatNumber (value, format, roundingFunction) { - var negP = false, - signed = false, - optDec = false, - abbr = '', - abbrK = false, // force abbreviation to thousands - abbrM = false, // force abbreviation to millions - abbrB = false, // force abbreviation to billions - abbrT = false, // force abbreviation to trillions - abbrForce = false, // force abbreviation - bytes = '', - ord = '', - abs = Math.abs(value), - suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], - min, - max, - power, - w, - precision, - thousands, - d = '', - neg = false; - - // check if number is zero and a custom zero format has been set - if (value === 0 && zeroFormat !== null) { - return zeroFormat; - } else { - // see if we should use parentheses for negative number or if we should prefix with a sign - // if both are present we default to parentheses - if (format.indexOf('(') > -1) { - negP = true; - format = format.slice(1, -1); - } else if (format.indexOf('+') > -1) { - signed = true; - format = format.replace(/\+/g, ''); - } - - // see if abbreviation is wanted - if (format.indexOf('a') > -1) { - // check if abbreviation is specified - abbrK = format.indexOf('aK') >= 0; - abbrM = format.indexOf('aM') >= 0; - abbrB = format.indexOf('aB') >= 0; - abbrT = format.indexOf('aT') >= 0; - abbrForce = abbrK || abbrM || abbrB || abbrT; - - // check for space before abbreviation - if (format.indexOf(' a') > -1) { - abbr = ' '; - format = format.replace(' a', ''); - } else { - format = format.replace('a', ''); - } - - if (abs >= Math.pow(10, 12) && !abbrForce || abbrT) { - // trillion - abbr = abbr + languages[currentLanguage].abbreviations.trillion; - value = value / Math.pow(10, 12); - } else if (abs < Math.pow(10, 12) && abs >= Math.pow(10, 9) && !abbrForce || abbrB) { - // billion - abbr = abbr + languages[currentLanguage].abbreviations.billion; - value = value / Math.pow(10, 9); - } else if (abs < Math.pow(10, 9) && abs >= Math.pow(10, 6) && !abbrForce || abbrM) { - // million - abbr = abbr + languages[currentLanguage].abbreviations.million; - value = value / Math.pow(10, 6); - } else if (abs < Math.pow(10, 6) && abs >= Math.pow(10, 3) && !abbrForce || abbrK) { - // thousand - abbr = abbr + languages[currentLanguage].abbreviations.thousand; - value = value / Math.pow(10, 3); - } - } - - // see if we are formatting bytes - if (format.indexOf('b') > -1) { - // check for space before - if (format.indexOf(' b') > -1) { - bytes = ' '; - format = format.replace(' b', ''); - } else { - format = format.replace('b', ''); - } - - for (power = 0; power <= suffixes.length; power++) { - min = Math.pow(1024, power); - max = Math.pow(1024, power+1); - - if (value >= min && value < max) { - bytes = bytes + suffixes[power]; - if (min > 0) { - value = value / min; - } - break; - } - } - } - - // see if ordinal is wanted - if (format.indexOf('o') > -1) { - // check for space before - if (format.indexOf(' o') > -1) { - ord = ' '; - format = format.replace(' o', ''); - } else { - format = format.replace('o', ''); - } - - ord = ord + languages[currentLanguage].ordinal(value); - } - - if (format.indexOf('[.]') > -1) { - optDec = true; - format = format.replace('[.]', '.'); - } - - w = value.toString().split('.')[0]; - precision = format.split('.')[1]; - thousands = format.indexOf(','); - - if (precision) { - if (precision.indexOf('[') > -1) { - precision = precision.replace(']', ''); - precision = precision.split('['); - d = toFixed(value, (precision[0].length + precision[1].length), roundingFunction, precision[1].length); - } else { - d = toFixed(value, precision.length, roundingFunction); - } - - w = d.split('.')[0]; - - if (d.split('.')[1].length) { - d = languages[currentLanguage].delimiters.decimal + d.split('.')[1]; - } else { - d = ''; - } - - if (optDec && Number(d.slice(1)) === 0) { - d = ''; - } - } else { - w = toFixed(value, null, roundingFunction); - } - - // format number - if (w.indexOf('-') > -1) { - w = w.slice(1); - neg = true; - } - - if (thousands > -1) { - w = w.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1' + languages[currentLanguage].delimiters.thousands); - } - - if (format.indexOf('.') === 0) { - w = ''; - } - - return ((negP && neg) ? '(' : '') + ((!negP && neg) ? '-' : '') + ((!neg && signed) ? '+' : '') + w + d + ((ord) ? ord : '') + ((abbr) ? abbr : '') + ((bytes) ? bytes : '') + ((negP && neg) ? ')' : ''); - } - } - - /************************************ - Top Level Functions - ************************************/ - - numeral = function (input) { - if (numeral.isNumeral(input)) { - input = input.value(); - } else if (input === 0 || typeof input === 'undefined') { - input = 0; - } else if (!Number(input)) { - input = numeral.fn.unformat(input); - } - - return new Numeral(Number(input)); - }; - - // version number - numeral.version = VERSION; - - // compare numeral object - numeral.isNumeral = function (obj) { - return obj instanceof Numeral; - }; - - // This function will load languages and then set the global language. If - // no arguments are passed in, it will simply return the current global - // language key. - numeral.language = function (key, values) { - if (!key) { - return currentLanguage; - } - - if (key && !values) { - if(!languages[key]) { - throw new Error('Unknown language : ' + key); - } - currentLanguage = key; - } - - if (values || !languages[key]) { - loadLanguage(key, values); - } - - return numeral; - }; - - // This function provides access to the loaded language data. If - // no arguments are passed in, it will simply return the current - // global language object. - numeral.languageData = function (key) { - if (!key) { - return languages[currentLanguage]; - } - - if (!languages[key]) { - throw new Error('Unknown language : ' + key); - } - - return languages[key]; - }; - - numeral.language('en', { - delimiters: { - thousands: ',', - decimal: '.' - }, - abbreviations: { - thousand: 'k', - million: 'm', - billion: 'b', - trillion: 't' - }, - ordinal: function (number) { - var b = number % 10; - return (~~ (number % 100 / 10) === 1) ? 'th' : - (b === 1) ? 'st' : - (b === 2) ? 'nd' : - (b === 3) ? 'rd' : 'th'; - }, - currency: { - symbol: '$' - } - }); - - numeral.zeroFormat = function (format) { - zeroFormat = typeof(format) === 'string' ? format : null; - }; - - numeral.defaultFormat = function (format) { - defaultFormat = typeof(format) === 'string' ? format : '0.0'; - }; - - /************************************ - Helpers - ************************************/ - - function loadLanguage(key, values) { - languages[key] = values; - } - - /************************************ - Floating-point helpers - ************************************/ - - // The floating-point helper functions and implementation - // borrows heavily from sinful.js: http://guipn.github.io/sinful.js/ - - /** - * Array.prototype.reduce for browsers that don't support it - * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce#Compatibility - */ - if ('function' !== typeof Array.prototype.reduce) { - Array.prototype.reduce = function (callback, opt_initialValue) { - 'use strict'; - - if (null === this || 'undefined' === typeof this) { - // At the moment all modern browsers, that support strict mode, have - // native implementation of Array.prototype.reduce. For instance, IE8 - // does not support strict mode, so this check is actually useless. - throw new TypeError('Array.prototype.reduce called on null or undefined'); - } - - if ('function' !== typeof callback) { - throw new TypeError(callback + ' is not a function'); - } - - var index, - value, - length = this.length >>> 0, - isValueSet = false; - - if (1 < arguments.length) { - value = opt_initialValue; - isValueSet = true; - } - - for (index = 0; length > index; ++index) { - if (this.hasOwnProperty(index)) { - if (isValueSet) { - value = callback(value, this[index], index, this); - } else { - value = this[index]; - isValueSet = true; - } - } - } - - if (!isValueSet) { - throw new TypeError('Reduce of empty array with no initial value'); - } - - return value; - }; - } - - - /** - * Computes the multiplier necessary to make x >= 1, - * effectively eliminating miscalculations caused by - * finite precision. - */ - function multiplier(x) { - var parts = x.toString().split('.'); - if (parts.length < 2) { - return 1; - } - return Math.pow(10, parts[1].length); - } - - /** - * Given a variable number of arguments, returns the maximum - * multiplier that must be used to normalize an operation involving - * all of them. - */ - function correctionFactor() { - var args = Array.prototype.slice.call(arguments); - return args.reduce(function (prev, next) { - var mp = multiplier(prev), - mn = multiplier(next); - return mp > mn ? mp : mn; - }, -Infinity); - } - - - /************************************ - Numeral Prototype - ************************************/ - - - numeral.fn = Numeral.prototype = { - - clone : function () { - return numeral(this); - }, - - format : function (inputString, roundingFunction) { - return formatNumeral(this, - inputString ? inputString : defaultFormat, - (roundingFunction !== undefined) ? roundingFunction : Math.round - ); - }, - - unformat : function (inputString) { - if (Object.prototype.toString.call(inputString) === '[object Number]') { - return inputString; - } - return unformatNumeral(this, inputString ? inputString : defaultFormat); - }, - - value : function () { - return this._value; - }, - - valueOf : function () { - return this._value; - }, - - set : function (value) { - this._value = Number(value); - return this; - }, - - add : function (value) { - var corrFactor = correctionFactor.call(null, this._value, value); - function cback(accum, curr, currI, O) { - return accum + corrFactor * curr; - } - this._value = [this._value, value].reduce(cback, 0) / corrFactor; - return this; - }, - - subtract : function (value) { - var corrFactor = correctionFactor.call(null, this._value, value); - function cback(accum, curr, currI, O) { - return accum - corrFactor * curr; - } - this._value = [value].reduce(cback, this._value * corrFactor) / corrFactor; - return this; - }, - - multiply : function (value) { - function cback(accum, curr, currI, O) { - var corrFactor = correctionFactor(accum, curr); - return (accum * corrFactor) * (curr * corrFactor) / - (corrFactor * corrFactor); - } - this._value = [this._value, value].reduce(cback, 1); - return this; - }, - - divide : function (value) { - function cback(accum, curr, currI, O) { - var corrFactor = correctionFactor(accum, curr); - return (accum * corrFactor) / (curr * corrFactor); - } - this._value = [this._value, value].reduce(cback); - return this; - }, - - difference : function (value) { - return Math.abs(numeral(this._value).subtract(value).value()); - } - - }; - - /************************************ - Exposing Numeral - ************************************/ - - // CommonJS module is defined - if (hasModule) { - module.exports = numeral; - } - - /*global ender:false */ - if (typeof ender === 'undefined') { - // here, `this` means `window` in the browser, or `global` on the server - // add `numeral` as a global object via a string identifier, - // for Closure Compiler 'advanced' mode - this['numeral'] = numeral; - } - - /*global define:false */ - if (typeof define === 'function' && define.amd) { - define([], function () { - return numeral; - }); - } -}).call(this); diff --git a/bower_components/handsontable/dist/jquery.handsontable.js b/bower_components/handsontable/dist/jquery.handsontable.js deleted file mode 100644 index 71cf9098..00000000 --- a/bower_components/handsontable/dist/jquery.handsontable.js +++ /dev/null @@ -1,11564 +0,0 @@ -/** - * Handsontable 0.9.19 - * Handsontable is a simple jQuery plugin for editable tables with basic copy-paste compatibility with Excel and Google Docs - * - * Copyright 2012, Marcin Warpechowski - * Licensed under the MIT license. - * http://handsontable.com/ - * - * Date: Tue Oct 01 2013 13:17:18 GMT+0200 (Central European Daylight Time) - */ -/*jslint white: true, browser: true, plusplus: true, indent: 4, maxerr: 50 */ - -var Handsontable = { //class namespace - extension: {}, //extenstion namespace - helper: {} //helper namespace -}; - -(function ($, window, Handsontable) { - "use strict"; -Handsontable.activeGuid = null; - -/** - * Handsontable constructor - * @param rootElement The jQuery element in which Handsontable DOM will be inserted - * @param userSettings - * @constructor - */ -Handsontable.Core = function (rootElement, userSettings) { - var priv - , datamap - , grid - , selection - , editproxy - , autofill - , instance = this - , GridSettings = function () { - }; - - Handsontable.helper.inherit(GridSettings, DefaultSettings); //create grid settings as a copy of default settings - Handsontable.helper.extend(GridSettings.prototype, Handsontable.TextCell); //overwrite defaults with default cell - expandType(userSettings); - Handsontable.helper.extend(GridSettings.prototype, userSettings); //overwrite defaults with user settings - - this.rootElement = rootElement; - var $document = $(document.documentElement); - var $body = $(document.body); - this.guid = 'ht_' + Handsontable.helper.randomString(); //this is the namespace for global events - - if (!this.rootElement[0].id) { - this.rootElement[0].id = this.guid; //if root element does not have an id, assign a random id - } - - priv = { - cellSettings: [], - columnSettings: [], - columnsSettingConflicts: ['data', 'width'], - settings: new GridSettings(), // current settings instance - settingsFromDOM: {}, - selStart: new Handsontable.SelectionPoint(), - selEnd: new Handsontable.SelectionPoint(), - editProxy: false, - isPopulated: null, - scrollable: null, - extensions: {}, - colToProp: null, - propToCol: null, - dataSchema: null, - dataType: 'array', - firstRun: true - }; - - datamap = { - recursiveDuckSchema: function (obj) { - var schema; - if ($.isPlainObject(obj)) { - schema = {}; - for (var i in obj) { - if (obj.hasOwnProperty(i)) { - if ($.isPlainObject(obj[i])) { - schema[i] = datamap.recursiveDuckSchema(obj[i]); - } - else { - schema[i] = null; - } - } - } - } - else { - schema = []; - } - return schema; - }, - - recursiveDuckColumns: function (schema, lastCol, parent) { - var prop, i; - if (typeof lastCol === 'undefined') { - lastCol = 0; - parent = ''; - } - if ($.isPlainObject(schema)) { - for (i in schema) { - if (schema.hasOwnProperty(i)) { - if (schema[i] === null) { - prop = parent + i; - priv.colToProp.push(prop); - priv.propToCol[prop] = lastCol; - lastCol++; - } - else { - lastCol = datamap.recursiveDuckColumns(schema[i], lastCol, i + '.'); - } - } - } - } - return lastCol; - }, - - createMap: function () { - if (typeof datamap.getSchema() === "undefined") { - throw new Error("trying to create `columns` definition but you didnt' provide `schema` nor `data`"); - } - var i, ilen, schema = datamap.getSchema(); - priv.colToProp = []; - priv.propToCol = {}; - if (priv.settings.columns) { - for (i = 0, ilen = priv.settings.columns.length; i < ilen; i++) { - priv.colToProp[i] = priv.settings.columns[i].data; - priv.propToCol[priv.settings.columns[i].data] = i; - } - } - else { - datamap.recursiveDuckColumns(schema); - } - }, - - colToProp: function (col) { - col = Handsontable.PluginHooks.execute(instance, 'modifyCol', col); - if (priv.colToProp && typeof priv.colToProp[col] !== 'undefined') { - return priv.colToProp[col]; - } - else { - return col; - } - }, - - propToCol: function (prop) { - var col; - if (typeof priv.propToCol[prop] !== 'undefined') { - col = priv.propToCol[prop]; - } - else { - col = prop; - } - col = Handsontable.PluginHooks.execute(instance, 'modifyCol', col); - return col; - }, - - getSchema: function () { - if (priv.settings.dataSchema) { - if (typeof priv.settings.dataSchema === 'function') { - return priv.settings.dataSchema(); - } - return priv.settings.dataSchema; - } - return priv.duckDataSchema; - }, - - /** - * Creates row at the bottom of the data array - * @param {Number} [index] Optional. Index of the row before which the new row will be inserted - */ - createRow: function (index, amount) { - var row - , colCount = instance.countCols() - , numberOfCreatedRows = 0 - , currentIndex; - - if (!amount) { - amount = 1; - } - - if (typeof index !== 'number' || index >= instance.countRows()) { - index = instance.countRows(); - } - - currentIndex = index; - while (numberOfCreatedRows < amount && instance.countRows() < priv.settings.maxRows) { - - if (priv.dataType === 'array') { - row = []; - for (var c = 0; c < colCount; c++) { - row.push(null); - } - } - else if (priv.dataType === 'function') { - row = priv.settings.dataSchema(index); - } - else { - row = $.extend(true, {}, datamap.getSchema()); - } - - if (index === instance.countRows()) { - GridSettings.prototype.data.push(row); - } - else { - GridSettings.prototype.data.splice(index, 0, row); - } - - numberOfCreatedRows++; - currentIndex++; - } - - - instance.PluginHooks.run('afterCreateRow', index, numberOfCreatedRows); - instance.forceFullRender = true; //used when data was changed - - return numberOfCreatedRows; - }, - - /** - * Creates col at the right of the data array - * @param {Number} [index] Optional. Index of the column before which the new column will be inserted - * * @param {Number} [amount] Optional. - */ - createCol: function (index, amount) { - if (priv.dataType === 'object' || priv.settings.columns) { - throw new Error("Cannot create new column. When data source in an object, you can only have as much columns as defined in first data row, data schema or in the 'columns' setting"); - } - var rlen = instance.countRows() - , data = GridSettings.prototype.data - , constructor - , numberOfCreatedCols = 0 - , currentIndex; - - if (!amount) { - amount = 1; - } - - currentIndex = index; - - while (numberOfCreatedCols < amount && instance.countCols() < priv.settings.maxCols){ - constructor = Handsontable.helper.columnFactory(GridSettings, priv.columnsSettingConflicts); - if (typeof index !== 'number' || index >= instance.countCols()) { - for (var r = 0; r < rlen; r++) { - if (typeof data[r] === 'undefined') { - data[r] = []; - } - data[r].push(null); - } - // Add new column constructor - priv.columnSettings.push(constructor); - } - else { - for (var r = 0 ; r < rlen; r++) { - data[r].splice(currentIndex, 0, null); - } - // Add new column constructor at given index - priv.columnSettings.splice(currentIndex, 0, constructor); - } - - numberOfCreatedCols++; - currentIndex++; - } - - instance.PluginHooks.run('afterCreateCol', index, numberOfCreatedCols); - instance.forceFullRender = true; //used when data was changed - - return numberOfCreatedCols; - }, - - /** - * Removes row from the data array - * @param {Number} [index] Optional. Index of the row to be removed. If not provided, the last row will be removed - * @param {Number} [amount] Optional. Amount of the rows to be removed. If not provided, one row will be removed - */ - removeRow: function (index, amount) { - if (!amount) { - amount = 1; - } - if (typeof index !== 'number') { - index = -amount; - } - - index = (instance.countRows() + index) % instance.countRows(); - - // We have to map the physical row ids to logical and than perform removing with (possibly) new row id - var logicRows = this.physicalRowsToLogical(index, amount); - - instance.PluginHooks.run('beforeRemoveRow', index, amount); - - var newData = GridSettings.prototype.data.filter(function (row, index) { - return logicRows.indexOf(index) == -1; - }); - - GridSettings.prototype.data.length = 0; - Array.prototype.push.apply(GridSettings.prototype.data, newData); - - instance.PluginHooks.run('afterRemoveRow', index, amount); - - instance.forceFullRender = true; //used when data was changed - }, - - /** - * Removes column from the data array - * @param {Number} [index] Optional. Index of the column to be removed. If not provided, the last column will be removed - * @param {Number} [amount] Optional. Amount of the columns to be removed. If not provided, one column will be removed - */ - removeCol: function (index, amount) { - if (priv.dataType === 'object' || priv.settings.columns) { - throw new Error("cannot remove column with object data source or columns option specified"); - } - if (!amount) { - amount = 1; - } - if (typeof index !== 'number') { - index = -amount; - } - - index = (instance.countCols() + index) % instance.countCols(); - - instance.PluginHooks.run('beforeRemoveCol', index, amount); - - var data = GridSettings.prototype.data; - for (var r = 0, rlen = instance.countRows(); r < rlen; r++) { - data[r].splice(index, amount); - } - priv.columnSettings.splice(index, amount); - - instance.PluginHooks.run('afterRemoveCol', index, amount); - instance.forceFullRender = true; //used when data was changed - }, - - /** - * Add / removes data from the column - * @param {Number} col Index of column in which do you want to do splice. - * @param {Number} index Index at which to start changing the array. If negative, will begin that many elements from the end - * @param {Number} amount An integer indicating the number of old array elements to remove. If amount is 0, no elements are removed - * param {...*} elements Optional. The elements to add to the array. If you don't specify any elements, spliceCol simply removes elements from the array - */ - spliceCol: function (col, index, amount/*, elements...*/) { - var elements = 4 <= arguments.length ? [].slice.call(arguments, 3) : []; - - var colData = instance.getDataAtCol(col); - var removed = colData.slice(index, index + amount); - var after = colData.slice(index + amount); - - Handsontable.helper.extendArray(elements, after); - var i = 0; - while (i < amount) { - elements.push(null); //add null in place of removed elements - i++; - } - Handsontable.helper.to2dArray(elements); - instance.populateFromArray(index, col, elements, null, null, 'spliceCol'); - - return removed; - }, - - /** - * Add / removes data from the row - * @param {Number} row Index of row in which do you want to do splice. - * @param {Number} index Index at which to start changing the array. If negative, will begin that many elements from the end - * @param {Number} amount An integer indicating the number of old array elements to remove. If amount is 0, no elements are removed - * param {...*} elements Optional. The elements to add to the array. If you don't specify any elements, spliceCol simply removes elements from the array - */ - spliceRow: function (row, index, amount/*, elements...*/) { - var elements = 4 <= arguments.length ? [].slice.call(arguments, 3) : []; - - var rowData = instance.getDataAtRow(row); - var removed = rowData.slice(index, index + amount); - var after = rowData.slice(index + amount); - - Handsontable.helper.extendArray(elements, after); - var i = 0; - while (i < amount) { - elements.push(null); //add null in place of removed elements - i++; - } - instance.populateFromArray(row, index, [elements], null, null, 'spliceRow'); - - return removed; - }, - - /** - * Returns single value from the data array - * @param {Number} row - * @param {Number} prop - */ - getVars: {}, - get: function (row, prop) { - datamap.getVars.row = row; - datamap.getVars.prop = prop; - instance.PluginHooks.run('beforeGet', datamap.getVars); - if (typeof datamap.getVars.prop === 'string' && datamap.getVars.prop.indexOf('.') > -1) { - var sliced = datamap.getVars.prop.split("."); - var out = priv.settings.data[datamap.getVars.row]; - if (!out) { - return null; - } - for (var i = 0, ilen = sliced.length; i < ilen; i++) { - out = out[sliced[i]]; - if (typeof out === 'undefined') { - return null; - } - } - return out; - } - else if (typeof datamap.getVars.prop === 'function') { - /** - * allows for interacting with complex structures, for example - * d3/jQuery getter/setter properties: - * - * {columns: [{ - * data: function(row, value){ - * if(arguments.length === 1){ - * return row.property(); - * } - * row.property(value); - * } - * }]} - */ - return datamap.getVars.prop(priv.settings.data.slice( - datamap.getVars.row, - datamap.getVars.row + 1 - )[0]); - } - else { - return priv.settings.data[datamap.getVars.row] ? priv.settings.data[datamap.getVars.row][datamap.getVars.prop] : null; - } - }, - - /** - * Saves single value to the data array - * @param {Number} row - * @param {Number} prop - * @param {String} value - * @param {String} [source] Optional. Source of hook runner. - */ - setVars: {}, - set: function (row, prop, value, source) { - datamap.setVars.row = row; - datamap.setVars.prop = prop; - datamap.setVars.value = value; - instance.PluginHooks.run('beforeSet', datamap.setVars, source || "datamapGet"); - if (typeof datamap.setVars.prop === 'string' && datamap.setVars.prop.indexOf('.') > -1) { - var sliced = datamap.setVars.prop.split("."); - var out = priv.settings.data[datamap.setVars.row]; - for (var i = 0, ilen = sliced.length - 1; i < ilen; i++) { - out = out[sliced[i]]; - } - out[sliced[i]] = datamap.setVars.value; - } - else if (typeof datamap.setVars.prop === 'function') { - /* see the `function` handler in `get` */ - datamap.setVars.prop(priv.settings.data.slice( - datamap.setVars.row, - datamap.setVars.row + 1 - )[0], datamap.setVars.value); - } - else { - priv.settings.data[datamap.setVars.row][datamap.setVars.prop] = datamap.setVars.value; - } - }, - /** - * This ridiculous piece of code maps rows Id that are present in table data to those displayed for user. - * The trick is, the physical row id (stored in settings.data) is not necessary the same - * as the logical (displayed) row id (e.g. when sorting is applied). - */ - physicalRowsToLogical: function (index, amount) { - var physicRow = (GridSettings.prototype.data.length + index) % GridSettings.prototype.data.length; - var logicRows = []; - var rowsToRemove = amount; - - while (physicRow < GridSettings.prototype.data.length && rowsToRemove) { - this.get(physicRow, 0); //this performs an actual mapping and saves the result to getVars - logicRows.push(this.getVars.row); - - rowsToRemove--; - physicRow++; - } - - return logicRows; - }, - - /** - * Clears the data array - */ - clear: function () { - for (var r = 0; r < instance.countRows(); r++) { - for (var c = 0; c < instance.countCols(); c++) { - datamap.set(r, datamap.colToProp(c), ''); - } - } - }, - - /** - * Returns the data array - * @return {Array} - */ - getAll: function () { - return priv.settings.data; - }, - - /** - * Returns data range as array - * @param {Object} start Start selection position - * @param {Object} end End selection position - * @return {Array} - */ - getRange: function (start, end) { - var r, rlen, c, clen, output = [], row; - rlen = Math.max(start.row, end.row); - clen = Math.max(start.col, end.col); - for (r = Math.min(start.row, end.row); r <= rlen; r++) { - row = []; - for (c = Math.min(start.col, end.col); c <= clen; c++) { - row.push(datamap.get(r, datamap.colToProp(c))); - } - output.push(row); - } - return output; - }, - - /** - * Return data as text (tab separated columns) - * @param {Object} start (Optional) Start selection position - * @param {Object} end (Optional) End selection position - * @return {String} - */ - getText: function (start, end) { - return SheetClip.stringify(datamap.getRange(start, end)); - } - }; - - grid = { - /** - * Inserts or removes rows and columns - * @param {String} action Possible values: "insert_row", "insert_col", "remove_row", "remove_col" - * @param {Number} index - * @param {Number} amount - * @param {String} [source] Optional. Source of hook runner. - * @param {Boolean} [keepEmptyRows] Optional. Flag for preventing deletion of empty rows. - */ - alter: function (action, index, amount, source, keepEmptyRows) { - var oldData, newData, changes, r, rlen, c, clen, delta; - oldData = $.extend(true, [], datamap.getAll()); - - amount = amount || 1; - - switch (action) { - case "insert_row": - delta = datamap.createRow(index, amount); - - if (delta) { - if (priv.selStart.exists() && priv.selStart.row() >= index) { - priv.selStart.row(priv.selStart.row() + delta); - selection.transformEnd(delta, 0); //will call render() internally - } - else { - selection.refreshBorders(); //it will call render and prepare methods - } - } - break; - - case "insert_col": - delta = datamap.createCol(index, amount); - - if (delta) { - - if(Handsontable.helper.isArray(instance.getSettings().colHeaders)){ - var spliceArray = [index, 0]; - spliceArray.length += delta; //inserts empty (undefined) elements at the end of an array - Array.prototype.splice.apply(instance.getSettings().colHeaders, spliceArray); //inserts empty (undefined) elements into the colHeader array - } - - if (priv.selStart.exists() && priv.selStart.col() >= index) { - priv.selStart.col(priv.selStart.col() + delta); - selection.transformEnd(0, delta); //will call render() internally - } - else { - selection.refreshBorders(); //it will call render and prepare methods - } - } - break; - - case "remove_row": - datamap.removeRow(index, amount); - priv.cellSettings.splice(index, amount); - grid.adjustRowsAndCols(); - selection.refreshBorders(); //it will call render and prepare methods - break; - - case "remove_col": - datamap.removeCol(index, amount); - - for(var row = 0, len = datamap.getAll().length; row < len; row++){ - if(row in priv.cellSettings){ //if row hasn't been rendered it wouldn't have cellSettings - priv.cellSettings[row].splice(index, amount); - } - } - - if(Handsontable.helper.isArray(instance.getSettings().colHeaders)){ - if(typeof index == 'undefined'){ - index = -1; - } - instance.getSettings().colHeaders.splice(index, amount); - } - - priv.columnSettings.splice(index, amount); - - grid.adjustRowsAndCols(); - selection.refreshBorders(); //it will call render and prepare methods - break; - - default: - throw new Error('There is no such action "' + action + '"'); - break; - } - - if (!keepEmptyRows) { - grid.adjustRowsAndCols(); //makes sure that we did not add rows that will be removed in next refresh - } - }, - - /** - * Makes sure there are empty rows at the bottom of the table - */ - adjustRowsAndCols: function () { - var r, rlen, emptyRows = instance.countEmptyRows(true), emptyCols; - - //should I add empty rows to data source to meet minRows? - rlen = instance.countRows(); - if (rlen < priv.settings.minRows) { - for (r = 0; r < priv.settings.minRows - rlen; r++) { - datamap.createRow(); - } - } - - //should I add empty rows to meet minSpareRows? - if (emptyRows < priv.settings.minSpareRows) { - for (; emptyRows < priv.settings.minSpareRows && instance.countRows() < priv.settings.maxRows; emptyRows++) { - datamap.createRow(); - } - } - - //count currently empty cols - emptyCols = instance.countEmptyCols(true); - - //should I add empty cols to meet minCols? - if (!priv.settings.columns && instance.countCols() < priv.settings.minCols) { - for (; instance.countCols() < priv.settings.minCols; emptyCols++) { - datamap.createCol(); - } - } - - //should I add empty cols to meet minSpareCols? - if (!priv.settings.columns && priv.dataType === 'array' && emptyCols < priv.settings.minSpareCols) { - for (; emptyCols < priv.settings.minSpareCols && instance.countCols() < priv.settings.maxCols; emptyCols++) { - datamap.createCol(); - } - } - - if (priv.settings.enterBeginsEditing) { - for (; (((priv.settings.minRows || priv.settings.minSpareRows) && instance.countRows() > priv.settings.minRows) && (priv.settings.minSpareRows && emptyRows > priv.settings.minSpareRows)); emptyRows--) { - datamap.removeRow(); - } - } - - if (priv.settings.enterBeginsEditing && !priv.settings.columns) { - for (; (((priv.settings.minCols || priv.settings.minSpareCols) && instance.countCols() > priv.settings.minCols) && (priv.settings.minSpareCols && emptyCols > priv.settings.minSpareCols)); emptyCols--) { - datamap.removeCol(); - } - } - - var rowCount = instance.countRows(); - var colCount = instance.countCols(); - - if (rowCount === 0 || colCount === 0) { - selection.deselect(); - } - - if (priv.selStart.exists()) { - var selectionChanged; - var fromRow = priv.selStart.row(); - var fromCol = priv.selStart.col(); - var toRow = priv.selEnd.row(); - var toCol = priv.selEnd.col(); - - //if selection is outside, move selection to last row - if (fromRow > rowCount - 1) { - fromRow = rowCount - 1; - selectionChanged = true; - if (toRow > fromRow) { - toRow = fromRow; - } - } else if (toRow > rowCount - 1) { - toRow = rowCount - 1; - selectionChanged = true; - if (fromRow > toRow) { - fromRow = toRow; - } - } - - //if selection is outside, move selection to last row - if (fromCol > colCount - 1) { - fromCol = colCount - 1; - selectionChanged = true; - if (toCol > fromCol) { - toCol = fromCol; - } - } else if (toCol > colCount - 1) { - toCol = colCount - 1; - selectionChanged = true; - if (fromCol > toCol) { - fromCol = toCol; - } - } - - if (selectionChanged) { - instance.selectCell(fromRow, fromCol, toRow, toCol); - } - } - }, - - /** - * Populate cells at position with 2d array - * @param {Object} start Start selection position - * @param {Array} input 2d array - * @param {Object} [end] End selection position (only for drag-down mode) - * @param {String} [source="populateFromArray"] - * @param {String} [method="overwrite"] - * @return {Object|undefined} ending td in pasted area (only if any cell was changed) - */ - populateFromArray: function (start, input, end, source, method) { - var r, rlen, c, clen, setData = [], current = {}; - rlen = input.length; - if (rlen === 0) { - return false; - } - - var repeatCol - , repeatRow - , cmax - , rmax; - - // insert data with specified pasteMode method - switch (method) { - case 'shift_down' : - repeatCol = end ? end.col - start.col + 1 : 0; - repeatRow = end ? end.row - start.row + 1 : 0; - input = Handsontable.helper.translateRowsToColumns(input); - for (c = 0, clen = input.length, cmax = Math.max(clen, repeatCol); c < cmax; c++) { - if (c < clen) { - for (r = 0, rlen = input[c].length; r < repeatRow - rlen; r++) { - input[c].push(input[c][r % rlen]); - } - input[c].unshift(start.col + c, start.row, 0); - instance.spliceCol.apply(instance, input[c]); - } - else { - input[c % clen][0] = start.col + c; - instance.spliceCol.apply(instance, input[c % clen]); - } - } - break; - - case 'shift_right' : - repeatCol = end ? end.col - start.col + 1 : 0; - repeatRow = end ? end.row - start.row + 1 : 0; - for (r = 0, rlen = input.length, rmax = Math.max(rlen, repeatRow); r < rmax; r++) { - if (r < rlen) { - for (c = 0, clen = input[r].length; c < repeatCol - clen; c++) { - input[r].push(input[r][c % clen]); - } - input[r].unshift(start.row + r, start.col, 0); - instance.spliceRow.apply(instance, input[r]); - } - else { - input[r % rlen][0] = start.row + r; - instance.spliceRow.apply(instance, input[r % rlen]); - } - } - break; - - case 'overwrite' : - default: - // overwrite and other not specified options - current.row = start.row; - current.col = start.col; - for (r = 0; r < rlen; r++) { - if ((end && current.row > end.row) || (!priv.settings.minSpareRows && current.row > instance.countRows() - 1) || (current.row >= priv.settings.maxRows)) { - break; - } - current.col = start.col; - clen = input[r] ? input[r].length : 0; - for (c = 0; c < clen; c++) { - if ((end && current.col > end.col) || (!priv.settings.minSpareCols && current.col > instance.countCols() - 1) || (current.col >= priv.settings.maxCols)) { - break; - } - if (!instance.getCellMeta(current.row, current.col).readOnly) { - setData.push([current.row, current.col, input[r][c]]); - } - current.col++; - if (end && c === clen - 1) { - c = -1; - } - } - current.row++; - if (end && r === rlen - 1) { - r = -1; - } - } - instance.setDataAtCell(setData, null, null, source || 'populateFromArray'); - break; - } - }, - - /** - * Returns the top left (TL) and bottom right (BR) selection coordinates - * @param {Object[]} coordsArr - * @returns {Object} - */ - getCornerCoords: function (coordsArr) { - function mapProp(func, array, prop) { - function getProp(el) { - return el[prop]; - } - - if (Array.prototype.map) { - return func.apply(Math, array.map(getProp)); - } - return func.apply(Math, $.map(array, getProp)); - } - - return { - TL: { - row: mapProp(Math.min, coordsArr, "row"), - col: mapProp(Math.min, coordsArr, "col") - }, - BR: { - row: mapProp(Math.max, coordsArr, "row"), - col: mapProp(Math.max, coordsArr, "col") - } - }; - }, - - /** - * Returns array of td objects given start and end coordinates - */ - getCellsAtCoords: function (start, end) { - var corners = grid.getCornerCoords([start, end]); - var r, c, output = []; - for (r = corners.TL.row; r <= corners.BR.row; r++) { - for (c = corners.TL.col; c <= corners.BR.col; c++) { - output.push(instance.view.getCellAtCoords({ - row: r, - col: c - })); - } - } - return output; - } - }; - - this.selection = selection = { //this public assignment is only temporary - inProgress: false, - - /** - * Sets inProgress to true. This enables onSelectionEnd and onSelectionEndByProp to function as desired - */ - begin: function () { - instance.selection.inProgress = true; - }, - - /** - * Sets inProgress to false. Triggers onSelectionEnd and onSelectionEndByProp - */ - finish: function () { - var sel = instance.getSelected(); - instance.PluginHooks.run("afterSelectionEnd", sel[0], sel[1], sel[2], sel[3]); - instance.PluginHooks.run("afterSelectionEndByProp", sel[0], instance.colToProp(sel[1]), sel[2], instance.colToProp(sel[3])); - instance.selection.inProgress = false; - }, - - isInProgress: function () { - return instance.selection.inProgress; - }, - - /** - * Starts selection range on given td object - * @param {Object} coords - */ - setRangeStart: function (coords) { - priv.selStart.coords(coords); - selection.setRangeEnd(coords); - }, - - /** - * Ends selection range on given td object - * @param {Object} coords - * @param {Boolean} [scrollToCell=true] If true, viewport will be scrolled to range end - */ - setRangeEnd: function (coords, scrollToCell) { - instance.selection.begin(); - - priv.selEnd.coords(coords); - if (!priv.settings.multiSelect) { - priv.selStart.coords(coords); - } - - //set up current selection - instance.view.wt.selections.current.clear(); - instance.view.wt.selections.current.add(priv.selStart.arr()); - - //set up area selection - instance.view.wt.selections.area.clear(); - if (selection.isMultiple()) { - instance.view.wt.selections.area.add(priv.selStart.arr()); - instance.view.wt.selections.area.add(priv.selEnd.arr()); - } - - //set up highlight - if (priv.settings.currentRowClassName || priv.settings.currentColClassName) { - instance.view.wt.selections.highlight.clear(); - instance.view.wt.selections.highlight.add(priv.selStart.arr()); - instance.view.wt.selections.highlight.add(priv.selEnd.arr()); - } - - //trigger handlers - instance.PluginHooks.run("afterSelection", priv.selStart.row(), priv.selStart.col(), priv.selEnd.row(), priv.selEnd.col()); - instance.PluginHooks.run("afterSelectionByProp", priv.selStart.row(), datamap.colToProp(priv.selStart.col()), priv.selEnd.row(), datamap.colToProp(priv.selEnd.col())); - - if (scrollToCell !== false) { - instance.view.scrollViewport(coords); - } - selection.refreshBorders(); - }, - - /** - * Destroys editor, redraws borders around cells, prepares editor - * @param {Boolean} revertOriginal - * @param {Boolean} keepEditor - */ - refreshBorders: function (revertOriginal, keepEditor) { - if (!keepEditor) { - editproxy.destroy(revertOriginal); - } - instance.view.render(); - if (selection.isSelected() && !keepEditor) { - editproxy.prepare(); - } - }, - - /** - * Returns information if we have a multiselection - * @return {Boolean} - */ - isMultiple: function () { - return !(priv.selEnd.col() === priv.selStart.col() && priv.selEnd.row() === priv.selStart.row()); - }, - - /** - * Selects cell relative to current cell (if possible) - */ - transformStart: function (rowDelta, colDelta, force) { - if (priv.selStart.row() + rowDelta > instance.countRows() - 1) { - if (force && priv.settings.minSpareRows > 0) { - instance.alter("insert_row", instance.countRows()); - } - else if (priv.settings.autoWrapCol) { - rowDelta = 1 - instance.countRows(); - colDelta = priv.selStart.col() + colDelta == instance.countCols() - 1 ? 1 - instance.countCols() : 1; - } - } - else if (priv.settings.autoWrapCol && priv.selStart.row() + rowDelta < 0 && priv.selStart.col() + colDelta >= 0) { - rowDelta = instance.countRows() - 1; - colDelta = priv.selStart.col() + colDelta == 0 ? instance.countCols() - 1 : -1; - } - - if (priv.selStart.col() + colDelta > instance.countCols() - 1) { - if (force && priv.settings.minSpareCols > 0) { - instance.alter("insert_col", instance.countCols()); - } - else if (priv.settings.autoWrapRow) { - rowDelta = priv.selStart.row() + rowDelta == instance.countRows() - 1 ? 1 - instance.countRows() : 1; - colDelta = 1 - instance.countCols(); - } - } - else if (priv.settings.autoWrapRow && priv.selStart.col() + colDelta < 0 && priv.selStart.row() + rowDelta >= 0) { - rowDelta = priv.selStart.row() + rowDelta == 0 ? instance.countRows() - 1 : -1; - colDelta = instance.countCols() - 1; - } - - var totalRows = instance.countRows(); - var totalCols = instance.countCols(); - var coords = { - row: priv.selStart.row() + rowDelta, - col: priv.selStart.col() + colDelta - }; - - if (coords.row < 0) { - coords.row = 0; - } - else if (coords.row > 0 && coords.row >= totalRows) { - coords.row = totalRows - 1; - } - - if (coords.col < 0) { - coords.col = 0; - } - else if (coords.col > 0 && coords.col >= totalCols) { - coords.col = totalCols - 1; - } - - selection.setRangeStart(coords); - }, - - /** - * Sets selection end cell relative to current selection end cell (if possible) - */ - transformEnd: function (rowDelta, colDelta) { - if (priv.selEnd.exists()) { - var totalRows = instance.countRows(); - var totalCols = instance.countCols(); - var coords = { - row: priv.selEnd.row() + rowDelta, - col: priv.selEnd.col() + colDelta - }; - - if (coords.row < 0) { - coords.row = 0; - } - else if (coords.row > 0 && coords.row >= totalRows) { - coords.row = totalRows - 1; - } - - if (coords.col < 0) { - coords.col = 0; - } - else if (coords.col > 0 && coords.col >= totalCols) { - coords.col = totalCols - 1; - } - - selection.setRangeEnd(coords); - } - }, - - /** - * Returns true if currently there is a selection on screen, false otherwise - * @return {Boolean} - */ - isSelected: function () { - return priv.selEnd.exists(); - }, - - /** - * Returns true if coords is within current selection coords - * @return {Boolean} - */ - inInSelection: function (coords) { - if (!selection.isSelected()) { - return false; - } - var sel = grid.getCornerCoords([priv.selStart.coords(), priv.selEnd.coords()]); - return (sel.TL.row <= coords.row && sel.BR.row >= coords.row && sel.TL.col <= coords.col && sel.BR.col >= coords.col); - }, - - /** - * Deselects all selected cells - */ - deselect: function () { - if (!selection.isSelected()) { - return; - } - instance.selection.inProgress = false; //needed by HT inception - priv.selEnd = new Handsontable.SelectionPoint(); //create new empty point to remove the existing one - instance.view.wt.selections.current.clear(); - instance.view.wt.selections.area.clear(); - editproxy.destroy(); - selection.refreshBorders(); - instance.PluginHooks.run('afterDeselect'); - }, - - /** - * Select all cells - */ - selectAll: function () { - if (!priv.settings.multiSelect) { - return; - } - selection.setRangeStart({ - row: 0, - col: 0 - }); - selection.setRangeEnd({ - row: instance.countRows() - 1, - col: instance.countCols() - 1 - }, false); - }, - - /** - * Deletes data from selected cells - */ - empty: function () { - if (!selection.isSelected()) { - return; - } - var corners = grid.getCornerCoords([priv.selStart.coords(), priv.selEnd.coords()]); - var r, c, changes = []; - for (r = corners.TL.row; r <= corners.BR.row; r++) { - for (c = corners.TL.col; c <= corners.BR.col; c++) { - if (!instance.getCellMeta(r, c).readOnly) { - changes.push([r, c, '']); - } - } - } - instance.setDataAtCell(changes); - } - }; - - this.autofill = autofill = { //this public assignment is only temporary - handle: null, - - /** - * Create fill handle and fill border objects - */ - init: function () { - if (!autofill.handle) { - autofill.handle = {}; - } - else { - autofill.handle.disabled = false; - } - }, - - /** - * Hide fill handle and fill border permanently - */ - disable: function () { - autofill.handle.disabled = true; - }, - - /** - * Selects cells down to the last row in the left column, then fills down to that cell - */ - selectAdjacent: function () { - var select, data, r, maxR, c; - - if (selection.isMultiple()) { - select = instance.view.wt.selections.area.getCorners(); - } - else { - select = instance.view.wt.selections.current.getCorners(); - } - - data = datamap.getAll(); - rows : for (r = select[2] + 1; r < instance.countRows(); r++) { - for (c = select[1]; c <= select[3]; c++) { - if (data[r][c]) { - break rows; - } - } - if (!!data[r][select[1] - 1] || !!data[r][select[3] + 1]) { - maxR = r; - } - } - if (maxR) { - instance.view.wt.selections.fill.clear(); - instance.view.wt.selections.fill.add([select[0], select[1]]); - instance.view.wt.selections.fill.add([maxR, select[3]]); - autofill.apply(); - } - }, - - /** - * Apply fill values to the area in fill border, omitting the selection border - */ - apply: function () { - var drag, select, start, end, _data; - - autofill.handle.isDragged = 0; - - drag = instance.view.wt.selections.fill.getCorners(); - if (!drag) { - return; - } - - instance.view.wt.selections.fill.clear(); - - if (selection.isMultiple()) { - select = instance.view.wt.selections.area.getCorners(); - } - else { - select = instance.view.wt.selections.current.getCorners(); - } - - if (drag[0] === select[0] && drag[1] < select[1]) { - start = { - row: drag[0], - col: drag[1] - }; - end = { - row: drag[2], - col: select[1] - 1 - }; - } - else if (drag[0] === select[0] && drag[3] > select[3]) { - start = { - row: drag[0], - col: select[3] + 1 - }; - end = { - row: drag[2], - col: drag[3] - }; - } - else if (drag[0] < select[0] && drag[1] === select[1]) { - start = { - row: drag[0], - col: drag[1] - }; - end = { - row: select[0] - 1, - col: drag[3] - }; - } - else if (drag[2] > select[2] && drag[1] === select[1]) { - start = { - row: select[2] + 1, - col: drag[1] - }; - end = { - row: drag[2], - col: drag[3] - }; - } - - if (start) { - - _data = SheetClip.parse(datamap.getText(priv.selStart.coords(), priv.selEnd.coords())); - instance.PluginHooks.run('beforeAutofill', start, end, _data); - - grid.populateFromArray(start, _data, end, 'autofill'); - - selection.setRangeStart({row: drag[0], col: drag[1]}); - selection.setRangeEnd({row: drag[2], col: drag[3]}); - } - /*else { - //reset to avoid some range bug - selection.refreshBorders(); - }*/ - }, - - /** - * Show fill border - */ - showBorder: function (coords) { - coords.row = coords[0]; - coords.col = coords[1]; - - var corners = grid.getCornerCoords([priv.selStart.coords(), priv.selEnd.coords()]); - if (priv.settings.fillHandle !== 'horizontal' && (corners.BR.row < coords.row || corners.TL.row > coords.row)) { - coords = [coords.row, corners.BR.col]; - } - else if (priv.settings.fillHandle !== 'vertical') { - coords = [corners.BR.row, coords.col]; - } - else { - return; //wrong direction - } - - instance.view.wt.selections.fill.clear(); - instance.view.wt.selections.fill.add([priv.selStart.coords().row, priv.selStart.coords().col]); - instance.view.wt.selections.fill.add([priv.selEnd.coords().row, priv.selEnd.coords().col]); - instance.view.wt.selections.fill.add(coords); - instance.view.render(); - } - }; - - editproxy = { //this public assignment is only temporary - /** - * Create input field - */ - init: function () { - priv.onCut = function onCut() { - if (!instance.isListening()) { - return; - } - - selection.empty(); - }; - - priv.onPaste = function onPaste(str) { - if (!instance.isListening() || !selection.isSelected()) { - return; - } - - var input = str.replace(/^[\r\n]*/g, '').replace(/[\r\n]*$/g, '') //remove newline from the start and the end of the input - , inputArray = SheetClip.parse(input) - , coords = grid.getCornerCoords([priv.selStart.coords(), priv.selEnd.coords()]) - , areaStart = coords.TL - , areaEnd = { - row: Math.max(coords.BR.row, inputArray.length - 1 + coords.TL.row), - col: Math.max(coords.BR.col, inputArray[0].length - 1 + coords.TL.col) - }; - - instance.PluginHooks.once('afterChange', function (changes, source) { - if (changes && changes.length) { - instance.selectCell(areaStart.row, areaStart.col, areaEnd.row, areaEnd.col); - } - }); - - grid.populateFromArray(areaStart, inputArray, areaEnd, 'paste', priv.settings.pasteMode); - }; - - function onKeyDown(event) { - if (!instance.isListening()) { - return; - } - - if (priv.settings.beforeOnKeyDown) { // HOT in HOT Plugin - priv.settings.beforeOnKeyDown.call(instance, event); - } - - if (Array.prototype.filter.call(document.body.querySelectorAll('.context-menu-list'), instance.view.wt.wtDom.isVisible).length) { //faster than $body.children('.context-menu-list:visible').length - //if right-click context menu is visible, do not execute this keydown handler (arrow keys will navigate the context menu) - return; - } - - if (event.keyCode === 17 || event.keyCode === 224 || event.keyCode === 91 || event.keyCode === 93) { - //when CTRL is pressed, prepare selectable text in textarea - //http://stackoverflow.com/questions/3902635/how-does-one-capture-a-macs-command-key-via-javascript - editproxy.setCopyableText(); - return; - } - - priv.lastKeyCode = event.keyCode; - if (selection.isSelected()) { - var ctrlDown = (event.ctrlKey || event.metaKey) && !event.altKey; //catch CTRL but not right ALT (which in some systems triggers ALT+CTRL) - if (Handsontable.helper.isPrintableChar(event.keyCode) && ctrlDown) { - if (event.keyCode === 65) { //CTRL + A - selection.selectAll(); //select all cells - editproxy.setCopyableText(); - event.preventDefault(); - event.stopImmediatePropagation(); - } - } - - var rangeModifier = event.shiftKey ? selection.setRangeEnd : selection.setRangeStart; - - instance.PluginHooks.run('beforeKeyDown', event); - if (!event.isImmediatePropagationStopped()) { - - switch (event.keyCode) { - case 38: /* arrow up */ - if (event.shiftKey) { - selection.transformEnd(-1, 0); - } - else { - selection.transformStart(-1, 0); - } - event.preventDefault(); - event.stopPropagation(); //required by HandsontableEditor - break; - - case 9: /* tab */ - var tabMoves = typeof priv.settings.tabMoves === 'function' ? priv.settings.tabMoves(event) : priv.settings.tabMoves; - if (event.shiftKey) { - selection.transformStart(-tabMoves.row, -tabMoves.col); //move selection left - } - else { - selection.transformStart(tabMoves.row, tabMoves.col, true); //move selection right (add a new column if needed) - } - event.preventDefault(); - event.stopPropagation(); //required by HandsontableEditor - break; - - case 39: /* arrow right */ - if (event.shiftKey) { - selection.transformEnd(0, 1); - } - else { - selection.transformStart(0, 1); - } - event.preventDefault(); - event.stopPropagation(); //required by HandsontableEditor - break; - - case 37: /* arrow left */ - if (event.shiftKey) { - selection.transformEnd(0, -1); - } - else { - selection.transformStart(0, -1); - } - event.preventDefault(); - event.stopPropagation(); //required by HandsontableEditor - break; - - case 8: /* backspace */ - case 46: /* delete */ - selection.empty(event); - event.preventDefault(); - break; - - case 40: /* arrow down */ - if (event.shiftKey) { - selection.transformEnd(1, 0); //expanding selection down with shift - } - else { - selection.transformStart(1, 0); //move selection down - } - event.preventDefault(); - event.stopPropagation(); //required by HandsontableEditor - break; - - case 113: /* F2 */ - event.preventDefault(); //prevent Opera from opening Go to Page dialog - break; - - case 13: /* return/enter */ - var enterMoves = typeof priv.settings.enterMoves === 'function' ? priv.settings.enterMoves(event) : priv.settings.enterMoves; - - if (event.shiftKey) { - selection.transformStart(-enterMoves.row, -enterMoves.col); //move selection up - } - else { - selection.transformStart(enterMoves.row, enterMoves.col, true); //move selection down (add a new row if needed) - } - - event.preventDefault(); //don't add newline to field - break; - - case 36: /* home */ - if (event.ctrlKey || event.metaKey) { - rangeModifier({row: 0, col: priv.selStart.col()}); - } - else { - rangeModifier({row: priv.selStart.row(), col: 0}); - } - event.preventDefault(); //don't scroll the window - event.stopPropagation(); //required by HandsontableEditor - break; - - case 35: /* end */ - if (event.ctrlKey || event.metaKey) { - rangeModifier({row: instance.countRows() - 1, col: priv.selStart.col()}); - } - else { - rangeModifier({row: priv.selStart.row(), col: instance.countCols() - 1}); - } - event.preventDefault(); //don't scroll the window - event.stopPropagation(); //required by HandsontableEditor - break; - - case 33: /* pg up */ - selection.transformStart(-instance.countVisibleRows(), 0); - instance.view.wt.scrollVertical(-instance.countVisibleRows()); - instance.view.render(); - event.preventDefault(); //don't page up the window - event.stopPropagation(); //required by HandsontableEditor - break; - - case 34: /* pg down */ - selection.transformStart(instance.countVisibleRows(), 0); - instance.view.wt.scrollVertical(instance.countVisibleRows()); - instance.view.render(); - event.preventDefault(); //don't page down the window - event.stopPropagation(); //required by HandsontableEditor - break; - - default: - break; - } - - } - } - } - - instance.copyPaste = CopyPaste.getInstance(); - instance.copyPaste.onCut(priv.onCut); - instance.copyPaste.onPaste(priv.onPaste); - $document.on('keydown.handsontable.' + instance.guid, onKeyDown); - }, - - /** - * Destroy current editor, if exists - * @param {Boolean} revertOriginal - */ - destroy: function (revertOriginal) { - if (typeof priv.editorDestroyer === "function") { - var destroyer = priv.editorDestroyer; //this copy is needed, otherwise destroyer can enter an infinite loop - priv.editorDestroyer = null; - destroyer(revertOriginal); - } - }, - - /** - * Prepares copyable text in the invisible textarea - */ - setCopyableText: function () { - var startRow = Math.min(priv.selStart.row(), priv.selEnd.row()); - var startCol = Math.min(priv.selStart.col(), priv.selEnd.col()); - var endRow = Math.max(priv.selStart.row(), priv.selEnd.row()); - var endCol = Math.max(priv.selStart.col(), priv.selEnd.col()); - var finalEndRow = Math.min(endRow, startRow + priv.settings.copyRowsLimit - 1); - var finalEndCol = Math.min(endCol, startCol + priv.settings.copyColsLimit - 1); - - instance.copyPaste.copyable(datamap.getText({row: startRow, col: startCol}, {row: finalEndRow, col: finalEndCol})); - - if (endRow !== finalEndRow || endCol !== finalEndCol) { - instance.PluginHooks.run("afterCopyLimit", endRow - startRow + 1, endCol - startCol + 1, priv.settings.copyRowsLimit, priv.settings.copyColsLimit); - } - }, - - /** - * Prepare text input to be displayed at given grid cell - */ - prepare: function () { - if (instance.getCellMeta(priv.selStart.row(), priv.selStart.col()).readOnly) { - return; - } - - var TD = instance.view.getCellAtCoords(priv.selStart.coords()); - priv.editorDestroyer = instance.view.applyCellTypeMethod('editor', TD, priv.selStart.row(), priv.selStart.col()); - //presumably TD can be removed from here. Cell editor should also listen for changes if editable cell is outside from viewport - } - }; - - this.init = function () { - instance.PluginHooks.run('beforeInit'); - editproxy.init(); - - this.updateSettings(priv.settings, true); - this.parseSettingsFromDOM(); - this.view = new Handsontable.TableView(this); - - this.forceFullRender = true; //used when data was changed - this.view.render(); - - if (typeof priv.firstRun === 'object') { - instance.PluginHooks.run('afterChange', priv.firstRun[0], priv.firstRun[1]); - priv.firstRun = false; - } - instance.PluginHooks.run('afterInit'); - }; - - function ValidatorsQueue() { //moved this one level up so it can be used in any function here. Probably this should be moved to a separate file - var resolved = false; - - return { - validatorsInQueue: 0, - addValidatorToQueue: function () { - this.validatorsInQueue++; - resolved = false; - }, - removeValidatorFormQueue: function () { - this.validatorsInQueue = this.validatorsInQueue - 1 < 0 ? 0 : this.validatorsInQueue - 1; - this.checkIfQueueIsEmpty(); - }, - onQueueEmpty: function () { - }, - checkIfQueueIsEmpty: function () { - if (this.validatorsInQueue == 0 && resolved == false) { - resolved = true; - this.onQueueEmpty(); - } - } - }; - } - - function validateChanges(changes, source, callback) { - var waitingForValidator = new ValidatorsQueue(); - waitingForValidator.onQueueEmpty = resolve; - - for (var i = changes.length - 1; i >= 0; i--) { - if (changes[i] === null) { - changes.splice(i, 1); - } - else { - var col = datamap.propToCol(changes[i][1]); - var logicalCol = instance.runHooksAndReturn('modifyCol', col); //column order may have changes, so we need to translate physical col index (stored in datasource) to logical (displayed to user) - var cellProperties = instance.getCellMeta(changes[i][0], logicalCol); - - if (cellProperties.dataType === 'number' && typeof changes[i][3] === 'string') { - if (changes[i][3].length > 0 && /^-?[\d\s]*\.?\d*$/.test(changes[i][3])) { - changes[i][3] = numeral().unformat(changes[i][3] || '0'); //numeral cannot unformat empty string - } - } - - if (cellProperties.validator) { - waitingForValidator.addValidatorToQueue(); - instance.validateCell(changes[i][3], cellProperties, (function (i, cellProperties) { - return function (result) { - if (typeof result !== 'boolean') { - throw new Error("Validation error: result is not boolean"); - } - if (result === false && cellProperties.allowInvalid === false) { - changes.splice(i, 1); - --i; - } - waitingForValidator.removeValidatorFormQueue(); - } - })(i, cellProperties) - , source); - } - } - } - waitingForValidator.checkIfQueueIsEmpty(); - - function resolve() { - var beforeChangeResult; - - if (changes.length) { - beforeChangeResult = instance.PluginHooks.execute("beforeChange", changes, source); - if (typeof beforeChangeResult === 'function') { - $.when(result).then(function () { - callback(); //called when async validators and async beforeChange are resolved - }); - } - else if (beforeChangeResult === false) { - changes.splice(0, changes.length); //invalidate all changes (remove everything from array) - } - } - if (typeof beforeChangeResult !== 'function') { - callback(); //called when async validators are resolved and beforeChange was not async - } - } - } - - /** - * Internal function to apply changes. Called after validateChanges - * @param {Array} changes Array in form of [row, prop, oldValue, newValue] - * @param {String} source String that identifies how this change will be described in changes array (useful in onChange callback) - */ - function applyChanges(changes, source) { - var i = changes.length - 1; - - if (i < 0) { - return; - } - - for (; 0 <= i; i--) { - if (changes[i] === null) { - changes.splice(i, 1); - continue; - } - - if (priv.settings.minSpareRows) { - while (changes[i][0] > instance.countRows() - 1) { - datamap.createRow(); - } - } - - if (priv.dataType === 'array' && priv.settings.minSpareCols) { - while (datamap.propToCol(changes[i][1]) > instance.countCols() - 1) { - datamap.createCol(); - } - } - - datamap.set(changes[i][0], changes[i][1], changes[i][3]); - } - - instance.forceFullRender = true; //used when data was changed - grid.adjustRowsAndCols(); - selection.refreshBorders(null, true); - instance.PluginHooks.run('afterChange', changes, source || 'edit'); - } - - this.validateCell = function (value, cellProperties, callback, source) { - var validator = cellProperties.validator; - - if (Object.prototype.toString.call(validator) === '[object RegExp]') { - validator = (function (validator) { - return function (value, callback) { - callback(validator.test(value)); - } - })(validator); - } - - if (typeof validator === 'function') { - value = instance.PluginHooks.execute("beforeValidate", value, cellProperties.row, cellProperties.prop, source); - - validator.call(cellProperties, value, function (valid) { - cellProperties.valid = valid; - valid = instance.PluginHooks.execute("afterValidate", valid, value, cellProperties.row, cellProperties.prop, source); - callback(valid); - }); - } - else { //resolve callback even if validator function was not found - cellProperties.valid = true; - callback(true); - } - }; - - function setDataInputToArray(row, prop_or_col, value) { - if (typeof row === "object") { //is it an array of changes - return row; - } - else if ($.isPlainObject(value)) { //backwards compatibility - return value; - } - else { - return [ - [row, prop_or_col, value] - ]; - } - } - - /** - * Set data at given cell - * @public - * @param {Number|Array} row or array of changes in format [[row, col, value], ...] - * @param {Number|String} col or source String - * @param {String} value - * @param {String} source String that identifies how this change will be described in changes array (useful in onChange callback) - */ - this.setDataAtCell = function (row, col, value, source) { - var input = setDataInputToArray(row, col, value) - , i - , ilen - , changes = [] - , prop; - - for (i = 0, ilen = input.length; i < ilen; i++) { - if (typeof input[i] !== 'object') { - throw new Error('Method `setDataAtCell` accepts row number or changes array of arrays as its first parameter'); - } - if (typeof input[i][1] !== 'number') { - throw new Error('Method `setDataAtCell` accepts row and column number as its parameters. If you want to use object property name, use method `setDataAtRowProp`'); - } - prop = datamap.colToProp(input[i][1]); - changes.push([ - input[i][0], - prop, - datamap.get(input[i][0], prop), - input[i][2] - ]); - } - - if (!source && typeof row === "object") { - source = col; - } - - validateChanges(changes, source, function () { - applyChanges(changes, source); - }); - }; - - - /** - * Set data at given row property - * @public - * @param {Number|Array} row or array of changes in format [[row, prop, value], ...] - * @param {String} prop or source String - * @param {String} value - * @param {String} source String that identifies how this change will be described in changes array (useful in onChange callback) - */ - this.setDataAtRowProp = function (row, prop, value, source) { - var input = setDataInputToArray(row, prop, value) - , i - , ilen - , changes = []; - - for (i = 0, ilen = input.length; i < ilen; i++) { - changes.push([ - input[i][0], - input[i][1], - datamap.get(input[i][0], input[i][1]), - input[i][2] - ]); - } - - if (!source && typeof row === "object") { - source = prop; - } - - validateChanges(changes, source, function () { - applyChanges(changes, source); - }); - }; - - /** - * Listen to document body keyboard input - */ - this.listen = function () { - Handsontable.activeGuid = instance.guid; - - if (document.activeElement && document.activeElement !== document.body) { - document.activeElement.blur(); - } - else if (!document.activeElement) { //IE - document.body.focus(); - } - }; - - /** - * Stop listening to document body keyboard input - */ - this.unlisten = function () { - Handsontable.activeGuid = null; - }; - - /** - * Returns true if current Handsontable instance is listening on document body keyboard input - */ - this.isListening = function () { - return Handsontable.activeGuid === instance.guid; - }; - - /** - * Destroys current editor, renders and selects current cell. If revertOriginal != true, edited data is saved - * @param {Boolean} revertOriginal - */ - this.destroyEditor = function (revertOriginal) { - selection.refreshBorders(revertOriginal); - }; - - /** - * Populate cells at position with 2d array - * @param {Number} row Start row - * @param {Number} col Start column - * @param {Array} input 2d array - * @param {Number=} endRow End row (use when you want to cut input when certain row is reached) - * @param {Number=} endCol End column (use when you want to cut input when certain column is reached) - * @param {String=} [source="populateFromArray"] - * @param {String=} [method="overwrite"] - * @return {Object|undefined} ending td in pasted area (only if any cell was changed) - */ - this.populateFromArray = function (row, col, input, endRow, endCol, source, method) { - if (typeof input !== 'object') { - throw new Error("populateFromArray parameter `input` must be an array"); //API changed in 0.9-beta2, let's check if you use it correctly - } - return grid.populateFromArray({row: row, col: col}, input, typeof endRow === 'number' ? {row: endRow, col: endCol} : null, source, method); - }; - - /** - * Adds/removes data from the column - * @param {Number} col Index of column in which do you want to do splice. - * @param {Number} index Index at which to start changing the array. If negative, will begin that many elements from the end - * @param {Number} amount An integer indicating the number of old array elements to remove. If amount is 0, no elements are removed - * param {...*} elements Optional. The elements to add to the array. If you don't specify any elements, spliceCol simply removes elements from the array - */ - this.spliceCol = function (col, index, amount/*, elements... */) { - return datamap.spliceCol.apply(null, arguments); - }; - - /** - * Adds/removes data from the row - * @param {Number} row Index of column in which do you want to do splice. - * @param {Number} index Index at which to start changing the array. If negative, will begin that many elements from the end - * @param {Number} amount An integer indicating the number of old array elements to remove. If amount is 0, no elements are removed - * param {...*} elements Optional. The elements to add to the array. If you don't specify any elements, spliceCol simply removes elements from the array - */ - this.spliceRow = function (row, index, amount/*, elements... */) { - return datamap.spliceRow.apply(null, arguments); - }; - - /** - * Returns the top left (TL) and bottom right (BR) selection coordinates - * @param {Object[]} coordsArr - * @returns {Object} - */ - this.getCornerCoords = function (coordsArr) { - return grid.getCornerCoords(coordsArr); - }; - - /** - * Returns current selection. Returns undefined if there is no selection. - * @public - * @return {Array} [`startRow`, `startCol`, `endRow`, `endCol`] - */ - this.getSelected = function () { //https://github.com/warpech/jquery-handsontable/issues/44 //cjl - if (selection.isSelected()) { - return [priv.selStart.row(), priv.selStart.col(), priv.selEnd.row(), priv.selEnd.col()]; - } - }; - - /** - * Parse settings from DOM and CSS - * @public - */ - this.parseSettingsFromDOM = function () { - var overflow = this.rootElement.css('overflow'); - if (overflow === 'scroll' || overflow === 'auto') { - this.rootElement[0].style.overflow = 'visible'; - priv.settingsFromDOM.overflow = overflow; - } - else if (priv.settings.width === void 0 || priv.settings.height === void 0) { - priv.settingsFromDOM.overflow = 'auto'; - } - - if (priv.settings.width === void 0) { - priv.settingsFromDOM.width = this.rootElement.width(); - } - else { - priv.settingsFromDOM.width = void 0; - } - - priv.settingsFromDOM.height = void 0; - if (priv.settings.height === void 0) { - if (priv.settingsFromDOM.overflow === 'scroll' || priv.settingsFromDOM.overflow === 'auto') { - //this needs to read only CSS/inline style and not actual height - //so we need to call getComputedStyle on cloned container - var clone = this.rootElement[0].cloneNode(false); - var parent = this.rootElement[0].parentNode; - if (parent) { - clone.removeAttribute('id'); - parent.appendChild(clone); - var computedHeight = parseInt(window.getComputedStyle(clone, null).getPropertyValue('height'), 10); - - if(isNaN(computedHeight) && clone.currentStyle){ - computedHeight = parseInt(clone.currentStyle.height, 10) - } - - if (computedHeight > 0) { - priv.settingsFromDOM.height = computedHeight; - } - parent.removeChild(clone); - } - } - } - }; - - /** - * Render visible data - * @public - */ - this.render = function () { - if (instance.view) { - instance.forceFullRender = true; //used when data was changed - instance.parseSettingsFromDOM(); - selection.refreshBorders(null, true); - } - }; - - /** - * Load data from array - * @public - * @param {Array} data - */ - this.loadData = function (data) { - if (!(data.push && data.splice)) { //check if data is array. Must use duck-type check so Backbone Collections also pass it - throw new Error("loadData only accepts array of objects or array of arrays (" + typeof data + " given)"); - } - - priv.isPopulated = false; - GridSettings.prototype.data = data; - - if (priv.settings.dataSchema instanceof Array || data[0] instanceof Array) { - priv.dataType = 'array'; - } - else if ($.isFunction(priv.settings.dataSchema)) { - priv.dataType = 'function'; - } - else { - priv.dataType = 'object'; - } - - if (data[0]) { - priv.duckDataSchema = datamap.recursiveDuckSchema(data[0]); - } - else { - priv.duckDataSchema = {}; - } - datamap.createMap(); - - grid.adjustRowsAndCols(); - instance.PluginHooks.run('afterLoadData'); - - if (priv.firstRun) { - priv.firstRun = [null, 'loadData']; - } - else { - instance.PluginHooks.run('afterChange', null, 'loadData'); - instance.render(); - } - priv.isPopulated = true; - }; - - /** - * Return the current data object (the same that was passed by `data` configuration option or `loadData` method). Optionally you can provide cell range `r`, `c`, `r2`, `c2` to get only a fragment of grid data - * @public - * @param {Number} r (Optional) From row - * @param {Number} c (Optional) From col - * @param {Number} r2 (Optional) To row - * @param {Number} c2 (Optional) To col - * @return {Array|Object} - */ - this.getData = function (r, c, r2, c2) { - if (typeof r === 'undefined') { - return datamap.getAll(); - } - else { - return datamap.getRange({row: r, col: c}, {row: r2, col: c2}); - } - }; - - /** - * Update settings - * @public - */ - this.updateSettings = function (settings, init) { - var i, r, rlen, c, clen; - - if (typeof settings.rows !== "undefined") { - throw new Error("'rows' setting is no longer supported. do you mean startRows, minRows or maxRows?"); - } - if (typeof settings.cols !== "undefined") { - throw new Error("'cols' setting is no longer supported. do you mean startCols, minCols or maxCols?"); - } - - for (i in settings) { - if (i === 'data') { - continue; //loadData will be triggered later - } - else { - if (instance.PluginHooks.hooks.persistent[i] !== void 0 || instance.PluginHooks.legacy[i] !== void 0) { - instance.PluginHooks.add(i, settings[i]); - } - else { - // Update settings - if (!init && settings.hasOwnProperty(i)) { - GridSettings.prototype[i] = settings[i]; - } - - //launch extensions - if (Handsontable.extension[i]) { - priv.extensions[i] = new Handsontable.extension[i](instance, settings[i]); - } - } - } - } - - // Load data or create data map - if (settings.data === void 0 && priv.settings.data === void 0) { - var data = []; - var row; - for (r = 0, rlen = priv.settings.startRows; r < rlen; r++) { - row = []; - for (c = 0, clen = priv.settings.startCols; c < clen; c++) { - row.push(null); - } - data.push(row); - } - instance.loadData(data); //data source created just now - } - else if (settings.data !== void 0) { - instance.loadData(settings.data); //data source given as option - } - else if (settings.columns !== void 0) { - datamap.createMap(); - } - - // Init columns constructors configuration - clen = instance.countCols(); - - //Clear cellSettings cache - priv.cellSettings.length = 0; - - if (clen > 0) { - var prop, proto, column; - - for (i = 0; i < clen; i++) { - priv.columnSettings[i] = Handsontable.helper.columnFactory(GridSettings, priv.columnsSettingConflicts); - - // shortcut for prototype - proto = priv.columnSettings[i].prototype; - - // Use settings provided by user - if (GridSettings.prototype.columns) { - column = GridSettings.prototype.columns[i]; - expandType(column); - Handsontable.helper.extend(proto, column); - } - } - } - - if (typeof settings.fillHandle !== "undefined") { - if (autofill.handle && settings.fillHandle === false) { - autofill.disable(); - } - else if (!autofill.handle && settings.fillHandle !== false) { - autofill.init(); - } - } - - - if (!init) { - instance.PluginHooks.run('afterUpdateSettings'); - } - - grid.adjustRowsAndCols(); - if (instance.view) { - instance.forceFullRender = true; //used when data was changed - selection.refreshBorders(null, true); - } - }; - - function expandType(obj) { - if (obj.hasOwnProperty('type')) { //ignore obj.prototype.type - var type - , i; - if (typeof obj.type === 'object') { - type = obj.type; - } - else if (typeof obj.type === 'string') { - type = Handsontable.cellTypes[obj.type]; - if (type === void 0) { - throw new Error('You declared cell type "' + obj.type + '" as a string that is not mapped to a known object. Cell type must be an object or a string mapped to an object in Handsontable.cellTypes'); - } - } - for (i in type) { - if (type.hasOwnProperty(i) && !obj.hasOwnProperty(i)) { - obj[i] = type[i]; - } - } - } - } - - /** - * Returns current settings object - * @return {Object} - */ - this.getSettings = function () { - return priv.settings; - }; - - /** - * Returns current settingsFromDOM object - * @return {Object} - */ - this.getSettingsFromDOM = function () { - return priv.settingsFromDOM; - }; - - /** - * Clears grid - * @public - */ - this.clear = function () { - selection.selectAll(); - selection.empty(); - }; - - /** - * Inserts or removes rows and columns - * @param {String} action See grid.alter for possible values - * @param {Number} index - * @param {Number} amount - * @param {String} [source] Optional. Source of hook runner. - * @param {Boolean} [keepEmptyRows] Optional. Flag for preventing deletion of empty rows. - * @public - */ - this.alter = function (action, index, amount, source, keepEmptyRows) { - grid.alter(action, index, amount, source, keepEmptyRows); - }; - - /** - * Returns
    element corresponding to params row, col - * @param {Number} row - * @param {Number} col - * @public - * @return {Element} - */ - this.getCell = function (row, col) { - return instance.view.getCellAtCoords({row: row, col: col}); - }; - - /** - * Returns property name associated with column number - * @param {Number} col - * @public - * @return {String} - */ - this.colToProp = function (col) { - return datamap.colToProp(col); - }; - - /** - * Returns column number associated with property name - * @param {String} prop - * @public - * @return {Number} - */ - this.propToCol = function (prop) { - return datamap.propToCol(prop); - }; - - /** - * Return value at `row`, `col` - * @param {Number} row - * @param {Number} col - * @public - * @return value (mixed data type) - */ - this.getDataAtCell = function (row, col) { - return datamap.get(row, datamap.colToProp(col)); - }; - - /** - * Return value at `row`, `prop` - * @param {Number} row - * @param {String} prop - * @public - * @return value (mixed data type) - */ - this.getDataAtRowProp = function (row, prop) { - return datamap.get(row, prop); - }; - - /** - * Return value at `col` - * @param {Number} col - * @public - * @return value (mixed data type) - */ - this.getDataAtCol = function (col) { - return [].concat.apply([], datamap.getRange({row: 0, col: col}, {row: priv.settings.data.length - 1, col: col})); - }; - - /** - * Return value at `prop` - * @param {String} prop - * @public - * @return value (mixed data type) - */ - this.getDataAtProp = function (prop) { - return [].concat.apply([], datamap.getRange({row: 0, col: datamap.propToCol(prop)}, {row: priv.settings.data.length - 1, col: datamap.propToCol(prop)})); - }; - - /** - * Return value at `row` - * @param {Number} row - * @public - * @return value (mixed data type) - */ - this.getDataAtRow = function (row) { - return priv.settings.data[row]; - }; - - /** - * Returns cell meta data object corresponding to params row, col - * @param {Number} row - * @param {Number} col - * @public - * @return {Object} - */ - this.getCellMeta = function (row, col) { - var prop = datamap.colToProp(col) - , cellProperties; - - row = translateRowIndex(row); - col = translateColIndex(col); - - if ("undefined" === typeof priv.columnSettings[col]) { - priv.columnSettings[col] = Handsontable.helper.columnFactory(GridSettings, priv.columnsSettingConflicts); - } - - if (!priv.cellSettings[row]) { - priv.cellSettings[row] = []; - } - if (!priv.cellSettings[row][col]) { - priv.cellSettings[row][col] = new priv.columnSettings[col](); - } - - cellProperties = priv.cellSettings[row][col]; //retrieve cellProperties from cache - - cellProperties.row = row; - cellProperties.col = col; - cellProperties.prop = prop; - cellProperties.instance = instance; - - instance.PluginHooks.run('beforeGetCellMeta', row, col, cellProperties); - expandType(cellProperties); //for `type` added in beforeGetCellMeta - - if (cellProperties.cells) { - var settings = cellProperties.cells.call(cellProperties, row, col, prop); - - if (settings) { - expandType(settings); //for `type` added in cells - Handsontable.helper.extend(cellProperties, settings); - } - } - - instance.PluginHooks.run('afterGetCellMeta', row, col, cellProperties); - - return cellProperties; - - /** - * If displayed rows order is different than the order of rows stored in memory (i.e. sorting is applied) - * we need to translate logical (stored) row index to physical (displayed) index. - * @param row - original row index - * @returns {int} translated row index - */ - function translateRowIndex(row){ - var getVars = {row: row}; - - instance.PluginHooks.execute('beforeGet', getVars); - - return getVars.row; - } - - /** - * If displayed columns order is different than the order of columns stored in memory (i.e. column were moved using manualColumnMove plugin) - * we need to translate logical (stored) column index to physical (displayed) index. - * @param col - original column index - * @returns {int} - translated column index - */ - function translateColIndex(col){ - return Handsontable.PluginHooks.execute(instance, 'modifyCol', col); // warning: this must be done after datamap.colToProp - } - }; - - /** - * Validates all cells using their validator functions and calls callback when finished. Does not render the view - * @param callback - */ - this.validateCells = function (callback) { - var waitingForValidator = new ValidatorsQueue(); - waitingForValidator.onQueueEmpty = callback; - - var i = instance.countRows() - 1; - while (i >= 0) { - var j = instance.countCols() - 1; - while (j >= 0) { - waitingForValidator.addValidatorToQueue(); - instance.validateCell(instance.getDataAtCell(i, j), instance.getCellMeta(i, j), function () { - waitingForValidator.removeValidatorFormQueue(); - }, 'validateCells'); - j--; - } - i--; - } - waitingForValidator.checkIfQueueIsEmpty(); - }; - - /** - * Return array of row headers (if they are enabled). If param `row` given, return header at given row as string - * @param {Number} row (Optional) - * @return {Array|String} - */ - this.getRowHeader = function (row) { - if (row === void 0) { - var out = []; - for (var i = 0, ilen = instance.countRows(); i < ilen; i++) { - out.push(instance.getRowHeader(i)); - } - return out; - } - else if (Object.prototype.toString.call(priv.settings.rowHeaders) === '[object Array]' && priv.settings.rowHeaders[row] !== void 0) { - return priv.settings.rowHeaders[row]; - } - else if (typeof priv.settings.rowHeaders === 'function') { - return priv.settings.rowHeaders(row); - } - else if (priv.settings.rowHeaders && typeof priv.settings.rowHeaders !== 'string' && typeof priv.settings.rowHeaders !== 'number') { - return row + 1; - } - else { - return priv.settings.rowHeaders; - } - }; - - /** - * Return array of column headers (if they are enabled). If param `col` given, return header at given column as string - * @param {Number} col (Optional) - * @return {Array|String} - */ - this.getColHeader = function (col) { - if (col === void 0) { - var out = []; - for (var i = 0, ilen = instance.countCols(); i < ilen; i++) { - out.push(instance.getColHeader(i)); - } - return out; - } - else { - col = Handsontable.PluginHooks.execute(instance, 'modifyCol', col); - - if (priv.settings.columns && priv.settings.columns[col] && priv.settings.columns[col].title) { - return priv.settings.columns[col].title; - } - else if (Object.prototype.toString.call(priv.settings.colHeaders) === '[object Array]' && priv.settings.colHeaders[col] !== void 0) { - return priv.settings.colHeaders[col]; - } - else if (typeof priv.settings.colHeaders === 'function') { - return priv.settings.colHeaders(col); - } - else if (priv.settings.colHeaders && typeof priv.settings.colHeaders !== 'string' && typeof priv.settings.colHeaders !== 'number') { - return Handsontable.helper.spreadsheetColumnLabel(col); - } - else { - return priv.settings.colHeaders; - } - } - }; - - /** - * Return column width from settings (no guessing). Private use intended - * @param {Number} col - * @return {Number} - */ - this._getColWidthFromSettings = function (col) { - var cellProperties = instance.getCellMeta(0, col); - var width = cellProperties.width; - if (width === void 0 || width === priv.settings.width) { - width = cellProperties.colWidths; - } - if (width !== void 0) { - switch (typeof width) { - case 'object': //array - width = width[col]; - break; - - case 'function': - width = width(col); - break; - } - if (typeof width === 'string') { - width = parseInt(width, 10); - } - } - return width; - }; - - /** - * Return column width - * @param {Number} col - * @return {Number} - */ - this.getColWidth = function (col) { - col = Handsontable.PluginHooks.execute(instance, 'modifyCol', col); - var response = { - width: instance._getColWidthFromSettings(col) - }; - if (!response.width) { - response.width = 50; - } - instance.PluginHooks.run('afterGetColWidth', col, response); - return response.width; - }; - - /** - * Return total number of rows in grid - * @return {Number} - */ - this.countRows = function () { - return priv.settings.data.length; - }; - - /** - * Return total number of columns in grid - * @return {Number} - */ - this.countCols = function () { - if (priv.dataType === 'object' || priv.dataType === 'function') { - if (priv.settings.columns && priv.settings.columns.length) { - return priv.settings.columns.length; - } - else { - return priv.colToProp.length; - } - } - else if (priv.dataType === 'array') { - if (priv.settings.columns && priv.settings.columns.length) { - return priv.settings.columns.length; - } - else if (priv.settings.data && priv.settings.data[0] && priv.settings.data[0].length) { - return priv.settings.data[0].length; - } - else { - return 0; - } - } - }; - - /** - * Return index of first visible row - * @return {Number} - */ - this.rowOffset = function () { - return instance.view.wt.getSetting('offsetRow'); - }; - - /** - * Return index of first visible column - * @return {Number} - */ - this.colOffset = function () { - return instance.view.wt.getSetting('offsetColumn'); - }; - - /** - * Return number of visible rows. Returns -1 if table is not visible - * @return {Number} - */ - this.countVisibleRows = function () { - return instance.view.wt.drawn ? instance.view.wt.wtTable.rowStrategy.countVisible() : -1; - }; - - /** - * Return number of visible columns. Returns -1 if table is not visible - * @return {Number} - */ - this.countVisibleCols = function () { - return instance.view.wt.drawn ? instance.view.wt.wtTable.columnStrategy.countVisible() : -1; - }; - - /** - * Return number of empty rows - * @return {Boolean} ending If true, will only count empty rows at the end of the data source - */ - this.countEmptyRows = function (ending) { - var i = instance.countRows() - 1 - , empty = 0; - while (i >= 0) { - datamap.get(i, 0); - - if (instance.isEmptyRow(datamap.getVars.row)) { - empty++; - } - else if (ending) { - break; - } - i--; - } - return empty; - }; - - /** - * Return number of empty columns - * @return {Boolean} ending If true, will only count empty columns at the end of the data source row - */ - this.countEmptyCols = function (ending) { - if (instance.countRows() < 1) { - return 0; - } - - var i = instance.countCols() - 1 - , empty = 0; - while (i >= 0) { - if (instance.isEmptyCol(i)) { - empty++; - } - else if (ending) { - break; - } - i--; - } - return empty; - }; - - /** - * Return true if the row at the given index is empty, false otherwise - * @param {Number} r Row index - * @return {Boolean} - */ - this.isEmptyRow = function (r) { - if (priv.settings.isEmptyRow) { - return priv.settings.isEmptyRow.call(instance, r); - } - - var val; - for (var c = 0, clen = instance.countCols(); c < clen; c++) { - val = instance.getDataAtCell(r, c); - if (val !== '' && val !== null && typeof val !== 'undefined') { - return false; - } - } - return true; - }; - - /** - * Return true if the column at the given index is empty, false otherwise - * @param {Number} c Column index - * @return {Boolean} - */ - this.isEmptyCol = function (c) { - if (priv.settings.isEmptyCol) { - return priv.settings.isEmptyCol.call(instance, c); - } - - var val; - for (var r = 0, rlen = instance.countRows(); r < rlen; r++) { - val = instance.getDataAtCell(r, c); - if (val !== '' && val !== null && typeof val !== 'undefined') { - return false; - } - } - return true; - }; - - /** - * Selects cell on grid. Optionally selects range to another cell - * @param {Number} row - * @param {Number} col - * @param {Number} [endRow] - * @param {Number} [endCol] - * @param {Boolean} [scrollToCell=true] If true, viewport will be scrolled to the selection - * @public - * @return {Boolean} - */ - this.selectCell = function (row, col, endRow, endCol, scrollToCell) { - if (typeof row !== 'number' || row < 0 || row >= instance.countRows()) { - return false; - } - if (typeof col !== 'number' || col < 0 || col >= instance.countCols()) { - return false; - } - if (typeof endRow !== "undefined") { - if (typeof endRow !== 'number' || endRow < 0 || endRow >= instance.countRows()) { - return false; - } - if (typeof endCol !== 'number' || endCol < 0 || endCol >= instance.countCols()) { - return false; - } - } - priv.selStart.coords({row: row, col: col}); - if (document.activeElement && document.activeElement !== document.documentElement && document.activeElement !== document.body) { - document.activeElement.blur(); //needed or otherwise prepare won't focus the cell. selectionSpec tests this (should move focus to selected cell) - } - instance.listen(); - if (typeof endRow === "undefined") { - selection.setRangeEnd({row: row, col: col}, scrollToCell); - } - else { - selection.setRangeEnd({row: endRow, col: endCol}, scrollToCell); - } - - instance.selection.finish(); - return true; - }; - - this.selectCellByProp = function (row, prop, endRow, endProp, scrollToCell) { - arguments[1] = datamap.propToCol(arguments[1]); - if (typeof arguments[3] !== "undefined") { - arguments[3] = datamap.propToCol(arguments[3]); - } - return instance.selectCell.apply(instance, arguments); - }; - - /** - * Deselects current sell selection on grid - * @public - */ - this.deselectCell = function () { - selection.deselect(); - }; - - /** - * Remove grid from DOM - * @public - */ - this.destroy = function () { - instance.clearTimeouts(); - if (instance.view) { //in case HT is destroyed before initialization has finished - instance.view.wt.destroy(); - } - instance.rootElement.empty(); - instance.rootElement.removeData('handsontable'); - instance.rootElement.off('.handsontable'); - $(window).off('.' + instance.guid); - $document.off('.' + instance.guid); - $body.off('.' + instance.guid); - instance.copyPaste.removeCallback(priv.onCut); - instance.copyPaste.removeCallback(priv.onPaste); - instance.PluginHooks.run('afterDestroy'); - }; - - /** - * Return Handsontable instance - * @public - * @return {Object} - */ - this.getInstance = function () { - return instance.rootElement.data("handsontable"); - }; - - (function () { - // Create new instance of plugin hooks - instance.PluginHooks = new Handsontable.PluginHookClass(); - - // Upgrade methods to call of global PluginHooks instance - var _run = instance.PluginHooks.run - , _exe = instance.PluginHooks.execute; - - instance.PluginHooks.run = function (key, p1, p2, p3, p4, p5) { - _run.call(this, instance, key, p1, p2, p3, p4, p5); - Handsontable.PluginHooks.run(instance, key, p1, p2, p3, p4, p5); - }; - - instance.PluginHooks.execute = function (key, p1, p2, p3, p4, p5) { - var globalHandlerResult = Handsontable.PluginHooks.execute(instance, key, p1, p2, p3, p4, p5); - var localHandlerResult = _exe.call(this, instance, key, globalHandlerResult, p2, p3, p4, p5); - - return typeof localHandlerResult == 'undefined' ? globalHandlerResult : localHandlerResult; - - }; - - // Map old API with new methods - instance.addHook = function () { - instance.PluginHooks.add.apply(instance.PluginHooks, arguments); - }; - instance.addHookOnce = function () { - instance.PluginHooks.once.apply(instance.PluginHooks, arguments); - }; - - instance.removeHook = function () { - instance.PluginHooks.remove.apply(instance.PluginHooks, arguments); - }; - - instance.runHooks = function () { - instance.PluginHooks.run.apply(instance.PluginHooks, arguments); - }; - instance.runHooksAndReturn = function () { - return instance.PluginHooks.execute.apply(instance.PluginHooks, arguments); - }; - - })(); - - this.timeouts = {}; - - /** - * Sets timeout. Purpose of this method is to clear all known timeouts when `destroy` method is called - * @public - */ - this.registerTimeout = function (key, handle, ms) { - clearTimeout(this.timeouts[key]); - this.timeouts[key] = setTimeout(handle, ms || 0); - }; - - /** - * Clears all known timeouts - * @public - */ - this.clearTimeouts = function () { - for (var key in this.timeouts) { - if (this.timeouts.hasOwnProperty(key)) { - clearTimeout(this.timeouts[key]); - } - } - }; - - /** - * Handsontable version - */ - this.version = '0.9.19'; //inserted by grunt from package.json -}; - -var DefaultSettings = function () { -}; -DefaultSettings.prototype = { - data: void 0, - width: void 0, - height: void 0, - startRows: 5, - startCols: 5, - minRows: 0, - minCols: 0, - maxRows: Infinity, - maxCols: Infinity, - minSpareRows: 0, - minSpareCols: 0, - multiSelect: true, - fillHandle: true, - fixedRowsTop: 0, - fixedColumnsLeft: 0, - outsideClickDeselects: true, - enterBeginsEditing: true, - enterMoves: {row: 1, col: 0}, - tabMoves: {row: 0, col: 1}, - autoWrapRow: false, - autoWrapCol: false, - copyRowsLimit: 1000, - copyColsLimit: 1000, - pasteMode: 'overwrite', - currentRowClassName: void 0, - currentColClassName: void 0, - stretchH: 'hybrid', - isEmptyRow: void 0, - isEmptyCol: void 0, - observeDOMVisibility: true, - allowInvalid: true, - invalidCellClassName: 'htInvalid', - fragmentSelection: false, - readOnly: false, - scrollbarModelV: 'dragdealer', - scrollbarModelH: 'dragdealer' -}; - -$.fn.handsontable = function (action) { - var i - , ilen - , args - , output - , userSettings - , $this = this.first() // Use only first element from list - , instance = $this.data('handsontable'); - - // Init case - if (typeof action !== 'string') { - userSettings = action || {}; - if (instance) { - instance.updateSettings(userSettings); - } - else { - instance = new Handsontable.Core($this, userSettings); - $this.data('handsontable', instance); - instance.init(); - } - - return $this; - } - // Action case - else { - args = []; - if (arguments.length > 1) { - for (i = 1, ilen = arguments.length; i < ilen; i++) { - args.push(arguments[i]); - } - } - - if (instance) { - if (typeof instance[action] !== 'undefined') { - output = instance[action].apply(instance, args); - } - else { - throw new Error('Handsontable do not provide action: ' + action); - } - } - - return output; - } -}; - -/** - * Handsontable TableView constructor - * @param {Object} instance - */ -Handsontable.TableView = function (instance) { - var that = this - , $window = $(window) - , $documentElement = $(document.documentElement); - - this.instance = instance; - this.settings = instance.getSettings(); - this.settingsFromDOM = instance.getSettingsFromDOM(); - - instance.rootElement.data('originalStyle', instance.rootElement[0].getAttribute('style')); //needed to retrieve original style in jsFiddle link generator in HT examples. may be removed in future versions - // in IE7 getAttribute('style') returns an object instead of a string, but we only support IE8+ - - instance.rootElement.addClass('handsontable'); - - var table = document.createElement('TABLE'); - table.className = 'htCore'; - this.THEAD = document.createElement('THEAD'); - table.appendChild(this.THEAD); - this.TBODY = document.createElement('TBODY'); - table.appendChild(this.TBODY); - - instance.$table = $(table); - instance.rootElement.prepend(instance.$table); - - instance.rootElement.on('mousedown.handsontable', function (event) { - if (!that.isTextSelectionAllowed(event.target)) { - event.preventDefault(); //disable text selection in Chrome - clearTextSelection(); - } - }); - - $documentElement.on('keyup.' + instance.guid, function (event) { - if (instance.selection.isInProgress() && !event.shiftKey) { - instance.selection.finish(); - } - }); - - var isMouseDown - , dragInterval; - - $documentElement.on('mouseup.' + instance.guid, function (event) { - if (instance.selection.isInProgress() && event.which === 1) { //is left mouse button - instance.selection.finish(); - } - - isMouseDown = false; - clearInterval(dragInterval); - dragInterval = null; - - if (instance.autofill.handle && instance.autofill.handle.isDragged) { - if (instance.autofill.handle.isDragged > 1) { - instance.autofill.apply(); - } - instance.autofill.handle.isDragged = 0; - } - - if (Handsontable.helper.isOutsideInput(document.activeElement)) { - instance.unlisten(); - } - }); - - $documentElement.on('mousedown.' + instance.guid, function (event) { - var next = event.target; - - if (next !== that.wt.wtTable.spreader) { //immediate click on "spreader" means click on the right side of vertical scrollbar - while (next !== document.documentElement) { - //X-HANDSONTABLE is the tag name in Web Components version of HOT. Removal of this breaks cell selection - if (next === null) { - return; //click on something that was a row but now is detached (possibly because your click triggered a rerender) - } - if (next === instance.rootElement[0] || next.nodeName === 'X-HANDSONTABLE' || next.id === 'context-menu-layer' || $(next).is('.context-menu-list') || $(next).is('.typeahead li')) { - return; //click inside container - } - next = next.parentNode; - } - } - - if (that.settings.outsideClickDeselects) { - instance.deselectCell(); - } - else { - instance.destroyEditor(); - } - }); - - instance.rootElement.on('mousedown.handsontable', '.dragdealer', function (event) { - instance.destroyEditor(); - }); - - instance.$table.on('selectstart', function (event) { - if (that.settings.fragmentSelection) { - return; - } - - //https://github.com/warpech/jquery-handsontable/issues/160 - //selectstart is IE only event. Prevent text from being selected when performing drag down in IE8 - event.preventDefault(); - }); - - instance.$table.on('mouseenter', function () { - if (dragInterval) { //if dragInterval was set (that means mouse was really outside of table, not over an element that is outside of in DOM - clearInterval(dragInterval); - dragInterval = null; - } - }); - - instance.$table.on('mouseleave', function (event) { - if (!(isMouseDown || (instance.autofill.handle && instance.autofill.handle.isDragged))) { - return; - } - - var tolerance = 1 //this is needed because width() and height() contains stuff like cell borders - , offset = that.wt.wtDom.offset(table) - , offsetTop = offset.top + tolerance - , offsetLeft = offset.left + tolerance - , width = that.containerWidth - that.wt.getSetting('scrollbarWidth') - 2 * tolerance - , height = that.containerHeight - that.wt.getSetting('scrollbarHeight') - 2 * tolerance - , method - , row = 0 - , col = 0 - , dragFn; - - if (event.pageY < offsetTop) { //top edge crossed - row = -1; - method = 'scrollVertical'; - } - else if (event.pageY >= offsetTop + height) { //bottom edge crossed - row = 1; - method = 'scrollVertical'; - } - else if (event.pageX < offsetLeft) { //left edge crossed - col = -1; - method = 'scrollHorizontal'; - } - else if (event.pageX >= offsetLeft + width) { //right edge crossed - col = 1; - method = 'scrollHorizontal'; - } - - if (method) { - dragFn = function () { - if (isMouseDown || (instance.autofill.handle && instance.autofill.handle.isDragged)) { - //instance.selection.transformEnd(row, col); - that.wt[method](row + col).draw(); - } - }; - dragFn(); - dragInterval = setInterval(dragFn, 100); - } - }); - - var clearTextSelection = function () { - //http://stackoverflow.com/questions/3169786/clear-text-selection-with-javascript - if (window.getSelection) { - if (window.getSelection().empty) { // Chrome - window.getSelection().empty(); - } else if (window.getSelection().removeAllRanges) { // Firefox - window.getSelection().removeAllRanges(); - } - } else if (document.selection) { // IE? - document.selection.empty(); - } - }; - - var walkontableConfig = { - table: table, - stretchH: this.settings.stretchH, - data: instance.getDataAtCell, - totalRows: instance.countRows, - totalColumns: instance.countCols, - scrollbarModelV: this.settings.scrollbarModelV, - scrollbarModelH: this.settings.scrollbarModelH, - offsetRow: 0, - offsetColumn: 0, - width: this.getWidth(), - height: this.getHeight(), - fixedColumnsLeft: function () { - return that.settings.fixedColumnsLeft; - }, - fixedRowsTop: function () { - return that.settings.fixedRowsTop; - }, - rowHeaders: function () { - return that.settings.rowHeaders ? [function (index, TH) { - that.appendRowHeader(index, TH); - }] : [] - }, - columnHeaders: function () { - return that.settings.colHeaders ? [function (index, TH) { - that.appendColHeader(index, TH); - }] : [] - }, - columnWidth: instance.getColWidth, - cellRenderer: function (row, column, TD) { - that.applyCellTypeMethod('renderer', TD, row, column); - }, - selections: { - current: { - className: 'current', - border: { - width: 2, - color: '#5292F7', - style: 'solid', - cornerVisible: function () { - return that.settings.fillHandle && !that.isCellEdited() && !instance.selection.isMultiple() - } - } - }, - area: { - className: 'area', - border: { - width: 1, - color: '#89AFF9', - style: 'solid', - cornerVisible: function () { - return that.settings.fillHandle && !that.isCellEdited() && instance.selection.isMultiple() - } - } - }, - highlight: { - highlightRowClassName: that.settings.currentRowClassName, - highlightColumnClassName: that.settings.currentColClassName - }, - fill: { - className: 'fill', - border: { - width: 1, - color: 'red', - style: 'solid' - } - } - }, - hideBorderOnMouseDownOver: function () { - return that.settings.fragmentSelection; - }, - onCellMouseDown: function (event, coords, TD) { - instance.listen(); - - isMouseDown = true; - var coordsObj = {row: coords[0], col: coords[1]}; - if (event.button === 2 && instance.selection.inInSelection(coordsObj)) { //right mouse button - //do nothing - } - else if (event.shiftKey) { - instance.selection.setRangeEnd(coordsObj); - } - else { - instance.selection.setRangeStart(coordsObj); - } - - - if (that.settings.afterOnCellMouseDown) { - that.settings.afterOnCellMouseDown.call(instance, event, coords, TD); - } - }, - /*onCellMouseOut: function (/*event, coords, TD* /) { - if (isMouseDown && that.settings.fragmentSelection === 'single') { - clearTextSelection(); //otherwise text selection blinks during multiple cells selection - } - },*/ - onCellMouseOver: function (event, coords/*, TD*/) { - var coordsObj = {row: coords[0], col: coords[1]}; - if (isMouseDown) { - /*if (that.settings.fragmentSelection === 'single') { - clearTextSelection(); //otherwise text selection blinks during multiple cells selection - }*/ - instance.selection.setRangeEnd(coordsObj); - } - else if (instance.autofill.handle && instance.autofill.handle.isDragged) { - instance.autofill.handle.isDragged++; - instance.autofill.showBorder(coords); - } - }, - onCellCornerMouseDown: function (event) { - instance.autofill.handle.isDragged = 1; - event.preventDefault(); - }, - onCellCornerDblClick: function () { - instance.autofill.selectAdjacent(); - }, - beforeDraw: function (force) { - that.beforeRender(force); - }, - onDraw: function(force){ - that.onDraw(force); - } - }; - - instance.PluginHooks.run('beforeInitWalkontable', walkontableConfig); - - this.wt = new Walkontable(walkontableConfig); - - $window.on('resize.' + instance.guid, function () { - instance.registerTimeout('resizeTimeout', function () { - instance.parseSettingsFromDOM(); - var newWidth = that.getWidth(); - var newHeight = that.getHeight(); - if (walkontableConfig.width !== newWidth || walkontableConfig.height !== newHeight) { - instance.forceFullRender = true; - that.render(); - walkontableConfig.width = newWidth; - walkontableConfig.height = newHeight; - } - }, 60); - }); - - $(that.wt.wtTable.spreader).on('mousedown.handsontable, contextmenu.handsontable', function (event) { - if (event.target === that.wt.wtTable.spreader && event.which === 3) { //right mouse button exactly on spreader means right clickon the right hand side of vertical scrollbar - event.stopPropagation(); - } - }); - - $documentElement.on('click.' + instance.guid, function () { - if (that.settings.observeDOMVisibility) { - if (that.wt.drawInterrupted) { - that.instance.forceFullRender = true; - that.render(); - } - } - }); -}; - -Handsontable.TableView.prototype.isTextSelectionAllowed = function (el) { - if (el.nodeName === 'TEXTAREA') { - return (true); - } - if (this.settings.fragmentSelection && this.wt.wtDom.isChildOf(el, this.TBODY)) { - return (true); - } - return false; -}; - -Handsontable.TableView.prototype.isCellEdited = function () { - return document.activeElement !== document.body; -}; - -Handsontable.TableView.prototype.getWidth = function () { - var val = this.settings.width !== void 0 ? this.settings.width : this.settingsFromDOM.width; - return typeof val === 'function' ? val() : val; -}; - -Handsontable.TableView.prototype.getHeight = function () { - var val = this.settings.height !== void 0 ? this.settings.height : this.settingsFromDOM.height; - return typeof val === 'function' ? val() : val; -}; - -Handsontable.TableView.prototype.beforeRender = function (force) { - if (force) { //force = did Walkontable decide to do full render - this.instance.PluginHooks.run('beforeRender', this.instance.forceFullRender); //this.instance.forceFullRender = did Handsontable request full render? - this.wt.update('width', this.getWidth()); - this.wt.update('height', this.getHeight()); - } -}; - -Handsontable.TableView.prototype.onDraw = function(force){ - if (force) { //force = did Walkontable decide to do full render - this.instance.PluginHooks.run('afterRender', this.instance.forceFullRender); //this.instance.forceFullRender = did Handsontable request full render? - } -}; - -Handsontable.TableView.prototype.render = function () { - this.wt.draw(!this.instance.forceFullRender); - this.instance.forceFullRender = false; - this.instance.rootElement.triggerHandler('render.handsontable'); -}; - -Handsontable.TableView.prototype.applyCellTypeMethod = function (methodName, td, row, col) { - var prop = this.instance.colToProp(col) - , cellProperties = this.instance.getCellMeta(row, col) - , method = Handsontable.helper.getCellMethod(methodName, cellProperties[methodName]); //methodName is 'renderer' or 'editor' - - var value = this.instance.getDataAtRowProp(row, prop); - var res = method(this.instance, td, row, col, prop, value, cellProperties); - - if (methodName === 'renderer') { - this.instance.PluginHooks.run('afterRenderer', td, row, col, prop, value, cellProperties); - } - - return res; -}; - -/** - * Returns td object given coordinates - */ -Handsontable.TableView.prototype.getCellAtCoords = function (coords) { - var td = this.wt.wtTable.getCell([coords.row, coords.col]); - if (td < 0) { //there was an exit code (cell is out of bounds) - return null; - } - else { - return td; - } -}; - -/** - * Scroll viewport to selection - * @param coords - */ -Handsontable.TableView.prototype.scrollViewport = function (coords) { - this.wt.scrollViewport([coords.row, coords.col]); -}; - -/** - * Append row header to a TH element - * @param row - * @param TH - */ -Handsontable.TableView.prototype.appendRowHeader = function (row, TH) { - if (row > -1) { - this.wt.wtDom.fastInnerHTML(TH, this.instance.getRowHeader(row)); - } - else { - this.wt.wtDom.empty(TH); - } -}; - -/** - * Append column header to a TH element - * @param col - * @param TH - */ -Handsontable.TableView.prototype.appendColHeader = function (col, TH) { - var DIV = document.createElement('DIV') - , SPAN = document.createElement('SPAN'); - - DIV.className = 'relative'; - SPAN.className = 'colHeader'; - - this.wt.wtDom.fastInnerHTML(SPAN, this.instance.getColHeader(col)); - DIV.appendChild(SPAN); - - while (TH.firstChild) { - TH.removeChild(TH.firstChild); //empty TH node - } - TH.appendChild(DIV); - this.instance.PluginHooks.run('afterGetColHeader', col, TH); -}; - -/** - * Given a element's left position relative to the viewport, returns maximum element width until the right edge of the viewport (before scrollbar) - * @param {Number} left - * @return {Number} - */ -Handsontable.TableView.prototype.maximumVisibleElementWidth = function (left) { - var rootWidth = this.wt.wtViewport.getWorkspaceWidth(); - return rootWidth - left; -}; - -/** - * Given a element's top position relative to the viewport, returns maximum element height until the bottom edge of the viewport (before scrollbar) - * @param {Number} top - * @return {Number} - */ -Handsontable.TableView.prototype.maximumVisibleElementHeight = function (top) { - var rootHeight = this.wt.wtViewport.getWorkspaceHeight(); - if(this.wt.isNativeScroll) { - return rootHeight; - } - return rootHeight - top; -}; - -/** - * DOM helper optimized for maximum performance - * It is recommended for Handsontable plugins and renderers, because it is much faster than jQuery - * @type {WalkonableDom} - */ -Handsontable.Dom = new WalkontableDom(); - -/** - * Returns true if keyCode represents a printable character - * @param {Number} keyCode - * @return {Boolean} - */ -Handsontable.helper.isPrintableChar = function (keyCode) { - return ((keyCode == 32) || //space - (keyCode >= 48 && keyCode <= 57) || //0-9 - (keyCode >= 96 && keyCode <= 111) || //numpad - (keyCode >= 186 && keyCode <= 192) || //;=,-./` - (keyCode >= 219 && keyCode <= 222) || //[]{}\|"' - keyCode >= 226 || //special chars (229 for Asian chars) - (keyCode >= 65 && keyCode <= 90)); //a-z -}; - -/** - * Converts a value to string - * @param value - * @return {String} - */ -Handsontable.helper.stringify = function (value) { - switch (typeof value) { - case 'string': - case 'number': - return value + ''; - break; - - case 'object': - if (value === null) { - return ''; - } - else { - return value.toString(); - } - break; - - case 'undefined': - return ''; - break; - - default: - return value.toString(); - } -}; - -/** - * Generates spreadsheet-like column names: A, B, C, ..., Z, AA, AB, etc - * @param index - * @returns {String} - */ -Handsontable.helper.spreadsheetColumnLabel = function (index) { - var dividend = index + 1; - var columnLabel = ''; - var modulo; - while (dividend > 0) { - modulo = (dividend - 1) % 26; - columnLabel = String.fromCharCode(65 + modulo) + columnLabel; - dividend = parseInt((dividend - modulo) / 26, 10); - } - return columnLabel; -}; - -/** - * Checks if value of n is a numeric one - * http://jsperf.com/isnan-vs-isnumeric/4 - * @param n - * @returns {boolean} - */ -Handsontable.helper.isNumeric = function (n) { - var t = typeof n; - return t == 'number' ? !isNaN(n) && isFinite(n) : - t == 'string' ? !n.length ? false : - n.length == 1 ? /\d/.test(n) : - /^\s*[+-]?\s*(?:(?:\d+(?:\.\d+)?(?:e[+-]?\d+)?)|(?:0x[a-f\d]+))\s*$/i.test(n) : - t == 'object' ? !!n && typeof n.valueOf() == "number" && !(n instanceof Date) : false; -}; - -/** - * Checks if child is a descendant of given parent node - * http://stackoverflow.com/questions/2234979/how-to-check-in-javascript-if-one-element-is-a-child-of-another - * @param parent - * @param child - * @returns {boolean} - */ -Handsontable.helper.isDescendant = function (parent, child) { - var node = child.parentNode; - while (node != null) { - if (node == parent) { - return true; - } - node = node.parentNode; - } - return false; -}; - -/** - * Generates a random hex string. Used as namespace for Handsontable instance events. - * @return {String} - 16 character random string: "92b1bfc74ec4" - */ -Handsontable.helper.randomString = function () { - function s4() { - return Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1); - } - - return s4() + s4() + s4() + s4(); -}; - -/** - * Inherit without without calling parent constructor, and setting `Child.prototype.constructor` to `Child` instead of `Parent`. - * Creates temporary dummy function to call it as constructor. - * Described in ticket: https://github.com/warpech/jquery-handsontable/pull/516 - * @param {Object} Child child class - * @param {Object} Parent parent class - * @return {Object} extended Child - */ -Handsontable.helper.inherit = function (Child, Parent) { - function Bridge() { - } - - Bridge.prototype = Parent.prototype; - Child.prototype = new Bridge(); - Child.prototype.constructor = Child; - return Child; -}; - -/** - * Perform shallow extend of a target object with extension's own properties - * @param {Object} target An object that will receive the new properties - * @param {Object} extension An object containing additional properties to merge into the target - */ -Handsontable.helper.extend = function (target, extension) { - for (var i in extension) { - if (extension.hasOwnProperty(i)) { - target[i] = extension[i]; - } - } -}; - -/** - * Factory for columns constructors. - * @param {Object} GridSettings - * @param {Array} conflictList - * @return {Object} ColumnSettings - */ -Handsontable.helper.columnFactory = function (GridSettings, conflictList) { - var i = 0, len = conflictList.length, ColumnSettings = function () { - }; - - // Inherit prototype from grid settings - ColumnSettings.prototype = new GridSettings(); - - // Clear conflict settings - for (; i < len; i++) { - ColumnSettings.prototype[conflictList[i]] = void 0; - } - - return ColumnSettings; -}; - -Handsontable.helper.translateRowsToColumns = function (input) { - var i - , ilen - , j - , jlen - , output = [] - , olen = 0; - - for (i = 0, ilen = input.length; i < ilen; i++) { - for (j = 0, jlen = input[i].length; j < jlen; j++) { - if (j == olen) { - output.push([]); - olen++; - } - output[j].push(input[i][j]) - } - } - return output; -}; - -Handsontable.helper.to2dArray = function (arr) { - var i = 0 - , ilen = arr.length; - while (i < ilen) { - arr[i] = [arr[i]]; - i++; - } -}; - -Handsontable.helper.extendArray = function (arr, extension) { - var i = 0 - , ilen = extension.length; - while (i < ilen) { - arr.push(extension[i]); - i++; - } -}; - -/** - * Returns cell renderer or editor function directly or through lookup map - */ -Handsontable.helper.getCellMethod = function (methodName, methodFunction) { - if (typeof methodFunction === 'string') { - var result = Handsontable.cellLookup[methodName][methodFunction]; - if (result === void 0) { - throw new Error('You declared cell ' + methodName + ' "' + methodFunction + '" as a string that is not mapped to a known function. Cell ' + methodName + ' must be a function or a string mapped to a function in Handsontable.cellLookup.' + methodName + ' lookup object'); - } - return result; - } - else { - return methodFunction; - } -}; - -/** - * Determines if the given DOM element is an input field placed outside of HOT. - * Notice: By 'input' we mean input, textarea and select nodes - * @param element - DOM element - * @returns {boolean} - */ -Handsontable.helper.isOutsideInput = function (element) { - var inputs = ['INPUT', 'SELECT', 'TEXTAREA']; - - return inputs.indexOf(element.nodeName) > -1 && element.className.indexOf('handsontableInput') == -1; -}; - -/** - * Determines whether given object is an Array. - * Note: String is not an Array - * @param {*} obj - * @returns {boolean} - */ -Handsontable.helper.isArray = function(obj){ - return Array.isArray ? Array.isArray(obj) : Object.prototype.toString.call(obj) == '[object Array]'; -}; -Handsontable.SelectionPoint = function () { - this._row = null; //private use intended - this._col = null; -}; - -Handsontable.SelectionPoint.prototype.exists = function () { - return (this._row !== null); -}; - -Handsontable.SelectionPoint.prototype.row = function (val) { - if (val !== void 0) { - this._row = val; - } - return this._row; -}; - -Handsontable.SelectionPoint.prototype.col = function (val) { - if (val !== void 0) { - this._col = val; - } - return this._col; -}; - -Handsontable.SelectionPoint.prototype.coords = function (coords) { - if (coords !== void 0) { - this._row = coords.row; - this._col = coords.col; - } - return { - row: this._row, - col: this._col - } -}; - -Handsontable.SelectionPoint.prototype.arr = function (arr) { - if (arr !== void 0) { - this._row = arr[0]; - this._col = arr[1]; - } - return [this._row, this._col] -}; -/** - * Default text renderer - * @param {Object} instance Handsontable instance - * @param {Element} TD Table cell where to render - * @param {Number} row - * @param {Number} col - * @param {String|Number} prop Row object property name - * @param value Value to render (remember to escape unsafe HTML before inserting to DOM!) - * @param {Object} cellProperties Cell properites (shared by cell renderer and editor) - */ -Handsontable.TextRenderer = function (instance, TD, row, col, prop, value, cellProperties) { - var escaped = Handsontable.helper.stringify(value); - instance.view.wt.wtDom.fastInnerText(TD, escaped); //this is faster than innerHTML. See: https://github.com/warpech/jquery-handsontable/wiki/JavaScript-&-DOM-performance-tips - if (cellProperties.readOnly) { - instance.view.wt.wtDom.addClass(TD, 'htDimmed'); - } - if (cellProperties.valid === false && cellProperties.invalidCellClassName) { - instance.view.wt.wtDom.addClass(TD, cellProperties.invalidCellClassName); - } -}; -var clonableTEXT = document.createElement('DIV'); -clonableTEXT.className = 'htAutocomplete'; - -var clonableARROW = document.createElement('DIV'); -clonableARROW.className = 'htAutocompleteArrow'; -clonableARROW.appendChild(document.createTextNode('\u25BC')); -//this is faster than innerHTML. See: https://github.com/warpech/jquery-handsontable/wiki/JavaScript-&-DOM-performance-tips - -/** - * Autocomplete renderer - * @param {Object} instance Handsontable instance - * @param {Element} TD Table cell where to render - * @param {Number} row - * @param {Number} col - * @param {String|Number} prop Row object property name - * @param value Value to render (remember to escape unsafe HTML before inserting to DOM!) - * @param {Object} cellProperties Cell properites (shared by cell renderer and editor) - */ -Handsontable.AutocompleteRenderer = function (instance, TD, row, col, prop, value, cellProperties) { - var TEXT = clonableTEXT.cloneNode(false); //this is faster than createElement - var ARROW = clonableARROW.cloneNode(true); //this is faster than createElement - - if (!instance.acArrowListener) { - //not very elegant but easy and fast - instance.acArrowListener = function () { - instance.view.wt.getSetting('onCellDblClick'); - }; - instance.rootElement.on('mouseup', '.htAutocompleteArrow', instance.acArrowListener); //this way we don't bind event listener to each arrow. We rely on propagation instead - } - - Handsontable.TextRenderer(instance, TEXT, row, col, prop, value, cellProperties); - - if (!TEXT.firstChild) { //http://jsperf.com/empty-node-if-needed - //otherwise empty fields appear borderless in demo/renderers.html (IE) - TEXT.appendChild(document.createTextNode('\u00A0')); //\u00A0 equals   for a text node - //this is faster than innerHTML. See: https://github.com/warpech/jquery-handsontable/wiki/JavaScript-&-DOM-performance-tips - } - - TEXT.appendChild(ARROW); - instance.view.wt.wtDom.empty(TD); //TODO identify under what circumstances this line can be removed - TD.appendChild(TEXT); -}; -var clonableINPUT = document.createElement('INPUT'); -clonableINPUT.className = 'htCheckboxRendererInput'; -clonableINPUT.type = 'checkbox'; -clonableINPUT.setAttribute('autocomplete', 'off'); - -/** - * Checkbox renderer - * @param {Object} instance Handsontable instance - * @param {Element} TD Table cell where to render - * @param {Number} row - * @param {Number} col - * @param {String|Number} prop Row object property name - * @param value Value to render (remember to escape unsafe HTML before inserting to DOM!) - * @param {Object} cellProperties Cell properites (shared by cell renderer and editor) - */ -Handsontable.CheckboxRenderer = function (instance, TD, row, col, prop, value, cellProperties) { - if (typeof cellProperties.checkedTemplate === "undefined") { - cellProperties.checkedTemplate = true; - } - if (typeof cellProperties.uncheckedTemplate === "undefined") { - cellProperties.uncheckedTemplate = false; - } - - instance.view.wt.wtDom.empty(TD); //TODO identify under what circumstances this line can be removed - - var INPUT = clonableINPUT.cloneNode(false); //this is faster than createElement - - if (value === cellProperties.checkedTemplate || value === Handsontable.helper.stringify(cellProperties.checkedTemplate)) { - INPUT.checked = true; - TD.appendChild(INPUT); - } - else if (value === cellProperties.uncheckedTemplate || value === Handsontable.helper.stringify(cellProperties.uncheckedTemplate)) { - TD.appendChild(INPUT); - } - else if (value === null) { //default value - INPUT.className += ' noValue'; - TD.appendChild(INPUT); - } - else { - instance.view.wt.wtDom.fastInnerText(TD, '#bad value#'); //this is faster than innerHTML. See: https://github.com/warpech/jquery-handsontable/wiki/JavaScript-&-DOM-performance-tips - } - - var $input = $(INPUT); - - if (cellProperties.readOnly) { - $input.on('click', function (event) { - event.preventDefault(); - }); - } - else { - $input.on('mousedown', function (event) { - event.stopPropagation(); //otherwise can confuse cell mousedown handler - }); - - $input.on('mouseup', function (event) { - event.stopPropagation(); //otherwise can confuse cell dblclick handler - }); - - $input.on('change', function(){ - if (this.checked) { - instance.setDataAtRowProp(row, prop, cellProperties.checkedTemplate); - } - else { - instance.setDataAtRowProp(row, prop, cellProperties.uncheckedTemplate); - } - }); - } - - if(!instance.CheckboxRenderer || !instance.CheckboxRenderer.beforeKeyDownHookBound){ - instance.CheckboxRenderer = { - beforeKeyDownHookBound : true - }; - - instance.addHook('beforeKeyDown', function(event){ - if(event.keyCode == 32){ - event.stopImmediatePropagation(); - event.preventDefault(); - - var selection = instance.getSelected(); - var cell, checkbox, cellProperties; - var selStart = { - row: Math.min(selection[0], selection[2]), - col: Math.min(selection[1], selection[3]) - }; - - var selEnd = { - row: Math.max(selection[0], selection[2]), - col: Math.max(selection[1], selection[3]) - }; - - for(var row = selStart.row; row <= selEnd.row; row++ ){ - for(var col = selEnd.col; col <= selEnd.col; col++){ - cell = instance.getCell(row, col); - cellProperties = instance.getCellMeta(row, col); - checkbox = cell.querySelectorAll('input[type=checkbox]'); - - if(checkbox.length > 0 && !cellProperties.readOnly){ - for(var i = 0, len = checkbox.length; i < len; i++){ - checkbox[i].checked = !checkbox[i].checked; - $(checkbox[i]).trigger('change'); - } - } - - } - } - } - }); - } - - return TD; -}; -/** - * Numeric cell renderer - * @param {Object} instance Handsontable instance - * @param {Element} TD Table cell where to render - * @param {Number} row - * @param {Number} col - * @param {String|Number} prop Row object property name - * @param value Value to render (remember to escape unsafe HTML before inserting to DOM!) - * @param {Object} cellProperties Cell properites (shared by cell renderer and editor) - */ -Handsontable.NumericRenderer = function (instance, TD, row, col, prop, value, cellProperties) { - if (Handsontable.helper.isNumeric(value)) { - if (typeof cellProperties.language !== 'undefined') { - numeral.language(cellProperties.language) - } - value = numeral(value).format(cellProperties.format || '0'); //docs: http://numeraljs.com/ - instance.view.wt.wtDom.addClass(TD, 'htNumeric'); - } - Handsontable.TextRenderer(instance, TD, row, col, prop, value, cellProperties); -}; -(function(Handosntable){ - Handsontable.PasswordRenderer = function (instance, TD, row, col, prop, value, cellProperties) { - Handsontable.TextRenderer.apply(this, arguments); - - value = TD.innerHTML; - - var hash; - var hashLength = cellProperties.hashLength || value.length; - var hashSymbol = cellProperties.hashSymbol || '*'; - - for( hash = ''; hash.split(hashSymbol).length - 1 < hashLength; hash += hashSymbol); - - instance.view.wt.wtDom.fastInnerHTML(TD, hash); - - }; -})(Handsontable); -function HandsontableTextEditorClass(instance) { - this.instance = instance; - this.createElements(); - this.bindEvents(); -} - -HandsontableTextEditorClass.prototype.createElements = function () { - this.STATE_VIRGIN = 'STATE_VIRGIN'; //before editing - this.STATE_EDITING = 'STATE_EDITING'; - this.STATE_WAITING = 'STATE_WAITING'; //waiting for async validation - this.STATE_FINISHED = 'STATE_FINISHED'; - - this.state = this.STATE_VIRGIN; - this.waitingEvent = null; - - this.wtDom = new WalkontableDom(); - - this.TEXTAREA = document.createElement('TEXTAREA'); - this.TEXTAREA.className = 'handsontableInput'; - this.textareaStyle = this.TEXTAREA.style; - this.textareaStyle.width = 0; - this.textareaStyle.height = 0; - this.$textarea = $(this.TEXTAREA); - - this.TEXTAREA_PARENT = document.createElement('DIV'); - this.TEXTAREA_PARENT.className = 'handsontableInputHolder'; - this.textareaParentStyle = this.TEXTAREA_PARENT.style; - this.textareaParentStyle.top = 0; - this.textareaParentStyle.left = 0; - this.textareaParentStyle.display = 'none'; - - this.$body = $(document.body); - - this.TEXTAREA_PARENT.appendChild(this.TEXTAREA); - this.instance.rootElement[0].appendChild(this.TEXTAREA_PARENT); - - var that = this; - Handsontable.PluginHooks.add('afterRender', function () { - that.instance.registerTimeout('refresh_editor_dimensions', function () { - that.refreshDimensions(); - }, 0); - }); -}; - -HandsontableTextEditorClass.prototype.bindEvents = function () { - var that = this; - - this.$textarea.off('.editor').on('keydown.editor', function (event) { - if(that.state === that.STATE_WAITING) { - event.stopImmediatePropagation(); - } - else { - that.waitingEvent = null; - } - - if (that.state !== that.STATE_EDITING) { - return; - } - - var ctrlDown = (event.ctrlKey || event.metaKey) && !event.altKey; //catch CTRL but not right ALT (which in some systems triggers ALT+CTRL) - - if (event.keyCode === 17 || event.keyCode === 224 || event.keyCode === 91 || event.keyCode === 93) { - //when CTRL or its equivalent is pressed and cell is edited, don't prepare selectable text in textarea - event.stopImmediatePropagation(); - return; - } - - switch (event.keyCode) { - case 38: /* arrow up */ - that.finishEditing(false); - break; - - case 40: /* arrow down */ - that.finishEditing(false); - break; - - case 9: /* tab */ - that.finishEditing(false); - event.preventDefault(); - break; - - case 39: /* arrow right */ - if (that.wtDom.getCaretPosition(that.TEXTAREA) === that.TEXTAREA.value.length) { - that.finishEditing(false); - } - else { - event.stopImmediatePropagation(); - } - break; - - case 37: /* arrow left */ - if (that.wtDom.getCaretPosition(that.TEXTAREA) === 0) { - that.finishEditing(false); - } - else { - event.stopImmediatePropagation(); - } - break; - - case 27: /* ESC */ - that.instance.destroyEditor(true); - event.stopImmediatePropagation(); - break; - - case 13: /* return/enter */ - var selected = that.instance.getSelected(); - var isMultipleSelection = !(selected[0] === selected[2] && selected[1] === selected[3]); - if ((event.ctrlKey && !isMultipleSelection) || event.altKey) { //if ctrl+enter or alt+enter, add new line - that.TEXTAREA.value = that.TEXTAREA.value + '\n'; - that.TEXTAREA.focus(); - event.stopImmediatePropagation(); - } - else { - that.finishEditing(false, ctrlDown); - } - event.preventDefault(); //don't add newline to field - break; - - default: - event.stopImmediatePropagation(); //backspace, delete, home, end, CTRL+A, CTRL+C, CTRL+V, CTRL+X should only work locally when cell is edited (not in table context) - break; - } - - if (that.state !== that.STATE_FINISHED && !event.isImmediatePropagationStopped()) { - that.waitingEvent = event; - event.stopImmediatePropagation(); - event.preventDefault(); - } - }); -}; - -HandsontableTextEditorClass.prototype.bindTemporaryEvents = function (td, row, col, prop, value, cellProperties) { - var that = this; - - this.state = this.STATE_VIRGIN; - - function onDblClick() { - that.TEXTAREA.value = that.originalValue; - that.instance.destroyEditor(); - that.beginEditing(row, col, prop, true); - } - - this.instance.view.wt.update('onCellDblClick', onDblClick); - - this.TD = td; - this.row = row; - this.col = col; - this.prop = prop; - this.originalValue = value; - this.cellProperties = cellProperties; - - this.beforeKeyDownHook = function beforeKeyDownHook (event) { - if (that.state !== that.STATE_VIRGIN) { - return; - } - - var ctrlDown = (event.ctrlKey || event.metaKey) && !event.altKey; //catch CTRL but not right ALT (which in some systems triggers ALT+CTRL) - - if (Handsontable.helper.isPrintableChar(event.keyCode)) { - //here we are whitelisting all possible printable chars that can open the editor. - //in future, if there are some problems to find out if a char is printable, it would be better to invert this - //check (blacklist of non-printable chars should be shorter than whitelist of printable chars) - - if (!ctrlDown) { //disregard CTRL-key shortcuts - that.beginEditing(row, col, prop); - event.stopImmediatePropagation(); - } - } - else if (event.keyCode === 113) { //f2 - that.beginEditing(row, col, prop, true); //show edit field - event.stopImmediatePropagation(); - event.preventDefault(); //prevent Opera from opening Go to Page dialog - } - else if (event.keyCode === 13 && that.instance.getSettings().enterBeginsEditing) { //enter - var selected = that.instance.getSelected(); - var isMultipleSelection = !(selected[0] === selected[2] && selected[1] === selected[3]); - if ((ctrlDown && !isMultipleSelection) || event.altKey) { //if ctrl+enter or alt+enter, add new line - that.beginEditing(row, col, prop, true, '\n'); //show edit field - } - else { - that.beginEditing(row, col, prop, true); //show edit field - } - event.preventDefault(); //prevent new line at the end of textarea - event.stopImmediatePropagation(); - } else if ([9, 33, 34, 35, 36, 37, 38, 39, 40].indexOf(event.keyCode) == -1){ // other non printable character - that.instance.addHookOnce('beforeKeyDown', beforeKeyDownHook); - } - }; - that.instance.addHookOnce('beforeKeyDown', this.beforeKeyDownHook); -}; - -HandsontableTextEditorClass.prototype.unbindTemporaryEvents = function () { - this.instance.removeHook('beforeKeyDown', this.beforeKeyDownHook); - this.instance.view.wt.update('onCellDblClick', null); -}; - -HandsontableTextEditorClass.prototype.beginEditing = function (row, col, prop, useOriginalValue, suffix) { - if (this.state !== this.STATE_VIRGIN) { - return; - } - - this.state = this.STATE_EDITING; - - this.row = row; - this.col = col; - this.prop = prop; - - var coords = {row: row, col: col}; - this.instance.view.scrollViewport(coords); //viewport must be scrolled and rerendered before TEXTAREA is positioned - this.instance.view.render(); - - this.$textarea.on('cut.editor', function (event) { - event.stopPropagation(); - }); - - this.$textarea.on('paste.editor', function (event) { - event.stopPropagation(); - }); - - if (useOriginalValue) { - this.TEXTAREA.value = Handsontable.helper.stringify(this.originalValue) + (suffix || ''); - } - else { - this.TEXTAREA.value = ''; - } - - this.refreshDimensions(); //need it instantly, to prevent https://github.com/warpech/jquery-handsontable/issues/348 - this.TEXTAREA.focus(); - this.wtDom.setCaretPosition(this.TEXTAREA, this.TEXTAREA.value.length); - this.instance.view.render(); //only rerender the selections (FillHandle should disappear when beginediting is triggered) -}; - -HandsontableTextEditorClass.prototype.refreshDimensions = function () { - if (this.state !== this.STATE_EDITING) { - return; - } - - ///start prepare textarea position - this.TD = this.instance.getCell(this.row, this.col); - var $td = $(this.TD); //because old td may have been scrolled out with scrollViewport - var currentOffset = this.wtDom.offset(this.TD); - var containerOffset = this.wtDom.offset(this.instance.rootElement[0]); - var scrollTop = this.instance.rootElement.scrollTop(); - var scrollLeft = this.instance.rootElement.scrollLeft(); - var editTop = currentOffset.top - containerOffset.top + scrollTop - 1; - var editLeft = currentOffset.left - containerOffset.left + scrollLeft - 1; - - var settings = this.instance.getSettings(); - var rowHeadersCount = settings.rowHeaders === false ? 0 : 1; - var colHeadersCount = settings.colHeaders === false ? 0 : 1; - - if (editTop < 0) { - editTop = 0; - } - if (editLeft < 0) { - editLeft = 0; - } - - if (rowHeadersCount > 0 && parseInt($td.css('border-top-width'), 10) > 0) { - editTop += 1; - } - if (colHeadersCount > 0 && parseInt($td.css('border-left-width'), 10) > 0) { - editLeft += 1; - } - - if ($.browser.msie && parseInt($.browser.version, 10) <= 7) { - editTop -= 1; - } - - this.textareaParentStyle.top = editTop + 'px'; - this.textareaParentStyle.left = editLeft + 'px'; - ///end prepare textarea position - - var width = $td.width() - , maxWidth = this.instance.view.maximumVisibleElementWidth(editLeft) - 10 //10 is TEXTAREAs border and padding - , height = $td.outerHeight() - 4 - , maxHeight = this.instance.view.maximumVisibleElementHeight(editTop) - 5; //10 is TEXTAREAs border and padding - - if (parseInt($td.css('border-top-width'), 10) > 0) { - height -= 1; - } - if (parseInt($td.css('border-left-width'), 10) > 0) { - if (rowHeadersCount > 0) { - width -= 1; - } - } - - //in future may change to pure JS http://stackoverflow.com/questions/454202/creating-a-textarea-with-auto-resize - this.$textarea.autoResize({ - minHeight: Math.min(height, maxHeight), - maxHeight: maxHeight, //TEXTAREA should never be wider than visible part of the viewport (should not cover the scrollbar) - minWidth: Math.min(width, maxWidth), - maxWidth: maxWidth, //TEXTAREA should never be wider than visible part of the viewport (should not cover the scrollbar) - animate: false, - extraSpace: 0 - }); - - this.textareaParentStyle.display = 'block'; -}; - -HandsontableTextEditorClass.prototype.saveValue = function (val, ctrlDown) { - if (ctrlDown) { //if ctrl+enter and multiple cells selected, behave like Excel (finish editing and apply to all cells) - var sel = this.instance.getSelected(); - this.instance.populateFromArray(sel[0], sel[1], val, sel[2], sel[3], 'edit'); - } - else { - this.instance.populateFromArray(this.row, this.col, val, null, null, 'edit'); - } -}; - -HandsontableTextEditorClass.prototype.finishEditing = function (isCancelled, ctrlDown) { - var hasValidator = false; - - if (this.state == this.STATE_WAITING || this.state == this.STATE_FINISHED) { - return; - } - - if (this.state == this.STATE_EDITING) { - var val; - - if (isCancelled) { - val = [ - [this.originalValue] - ]; - } else { - val = [ - [$.trim(this.TEXTAREA.value)] - ]; - } - - hasValidator = this.instance.getCellMeta(this.row, this.col).validator; - - if (hasValidator) { - this.state = this.STATE_WAITING; - var that = this; - this.instance.addHookOnce('afterValidate', function (result) { - that.state = that.STATE_FINISHED; - that.discardEditor(result); - }); - } - this.saveValue(val, ctrlDown); - } - - if (!hasValidator) { - this.state = this.STATE_FINISHED; - this.discardEditor(); - } -}; - -HandsontableTextEditorClass.prototype.discardEditor = function (result) { - if (this.state !== this.STATE_FINISHED) { - return; - } - - if (result === false && this.cellProperties.allowInvalid !== true) { //validator was defined and failed - this.state = this.STATE_EDITING; - if (this.instance.view.wt.wtDom.isVisible(this.TEXTAREA)) { - this.TEXTAREA.focus(); - this.wtDom.setCaretPosition(this.TEXTAREA, this.TEXTAREA.value.length); - } - } - else { - this.state = this.STATE_FINISHED; - if (document.activeElement === this.TEXTAREA) { - this.instance.listen(); //don't refocus the table if user focused some cell outside of HT on purpose - } - this.unbindTemporaryEvents(); - - this.textareaParentStyle.display = 'none'; - - if (this.waitingEvent) { //this is needed so when you finish editing with Enter key, the default Enter behavior (move selection down) will work after async validation - var ev = $.Event(this.waitingEvent.type); - ev.keyCode = this.waitingEvent.keyCode; - this.waitingEvent = null; - $(document.activeElement).trigger(ev); - } - } -}; - -/** - * Default text editor - * @param {Object} instance Handsontable instance - * @param {Element} td Table cell where to render - * @param {Number} row - * @param {Number} col - * @param {String|Number} prop Row object property name - * @param value Original value (remember to escape unsafe HTML before inserting to DOM!) - * @param {Object} cellProperties Cell properites (shared by cell renderer and editor) - */ -Handsontable.TextEditor = function (instance, td, row, col, prop, value, cellProperties) { - if (!instance.textEditor) { - instance.textEditor = new HandsontableTextEditorClass(instance); - } - if (instance.textEditor.state === instance.textEditor.STATE_VIRGIN || instance.textEditor.state === instance.textEditor.STATE_FINISHED) { - instance.textEditor.bindTemporaryEvents(td, row, col, prop, value, cellProperties); - } - return function (isCancelled) { - instance.textEditor.finishEditing(isCancelled); - } -}; -function HandsontableAutocompleteEditorClass(instance) { - this.instance = instance; - this.createElements(); - this.bindEvents(); - this.emptyStringLabel = '\u00A0\u00A0\u00A0'; //3 non-breaking spaces -} - -Handsontable.helper.inherit(HandsontableAutocompleteEditorClass, HandsontableTextEditorClass); - -/** - * @see HandsontableTextEditorClass.prototype.createElements - */ -HandsontableAutocompleteEditorClass.prototype.createElements = function () { - HandsontableTextEditorClass.prototype.createElements.call(this); - - this.$textarea.typeahead(); - this.typeahead = this.$textarea.data('typeahead'); - this.typeahead._render = this.typeahead.render; - this.typeahead.minLength = 0; - - this.typeahead.lookup = function () { - var items; - this.query = this.$element.val(); - items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source; - return items ? this.process(items) : this; - }; - - this.typeahead.matcher = function () { - return true; - }; - - var _process = this.typeahead.process; - var that = this; - this.typeahead.process = function (items) { - var cloned = false; - for (var i = 0, ilen = items.length; i < ilen; i++) { - if (items[i] === '') { - //this is needed because because of issue #254 - //empty string ('') is a falsy value and breaks the loop in bootstrap-typeahead.js method `sorter` - //best solution would be to change line: `while (item = items.shift()) {` - // to: `while ((item = items.shift()) !== void 0) {` - if (!cloned) { - //need to clone items before applying emptyStringLabel - //(otherwise validateChanges fails for empty string) - items = $.extend([], items); - cloned = true; - } - items[i] = that.emptyStringLabel; - } - } - return _process.call(this, items); - }; -}; - -/** - * @see HandsontableTextEditorClass.prototype.bindEvents - */ -HandsontableAutocompleteEditorClass.prototype.bindEvents = function () { - var that = this; - - this.$textarea.off('keydown').off('keyup').off('keypress'); //unlisten - - this.$textarea.off('.acEditor').on('keydown.acEditor', function (event) { - switch (event.keyCode) { - case 38: /* arrow up */ - that.typeahead.prev(); - event.stopImmediatePropagation(); //stops TextEditor and core onKeyDown handler - break; - - case 40: /* arrow down */ - that.typeahead.next(); - event.stopImmediatePropagation(); //stops TextEditor and core onKeyDown handler - break; - - case 13: /* enter */ - event.preventDefault(); - break; - } - }); - - this.$textarea.on('keyup.acEditor', function (event) { - if (Handsontable.helper.isPrintableChar(event.keyCode) || event.keyCode === 113 || event.keyCode === 13 || event.keyCode === 8 || event.keyCode === 46) { - that.typeahead.lookup(); - } - }); - - this.typeahead.$menu.on('mouseleave', function(){ - that.typeahead.$menu.find('.active').removeClass('active'); - }); - - - HandsontableTextEditorClass.prototype.bindEvents.call(this); -}; -/** - * @see HandsontableTextEditorClass.prototype.bindTemporaryEvents - */ -HandsontableAutocompleteEditorClass.prototype.bindTemporaryEvents = function (td, row, col, prop, value, cellProperties) { - var that = this - , i - , j; - - this.typeahead._valueSelected = false; - - this.typeahead.select = function () { - var active = this.$menu[0].querySelector('.active'); - var val = active.getAttribute('data-value'); - if (val === that.emptyStringLabel) { - val = ''; - } - if (typeof cellProperties.onSelect === 'function') { - cellProperties.onSelect(row, col, prop, val, that.instance.view.wt.wtDom.index(active)); - } - else { - that.TEXTAREA.value = val; - } - - this._valueSelected = true; - - this.hide(); //need to hide it before destroyEditor, because destroyEditor checks if menu is expanded - that.finishEditing(); - - return this; - }; - - this.typeahead.render = function (items) { - that.typeahead._render.call(this, items); - if (!cellProperties.strict) { - var li = this.$menu[0].querySelector('.active'); - if (li) { - that.instance.view.wt.wtDom.removeClass(li, 'active') - } - } - return this; - }; - - /* overwrite typeahead options and methods (matcher, sorter, highlighter, updater, etc) if provided in cellProperties */ - for (i in cellProperties) { - if (i === 'options') { - for (j in cellProperties.options) { - this.typeahead.options[j] = cellProperties.options[j]; - } - } - else { - this.typeahead[i] = cellProperties[i]; - } - } - - HandsontableTextEditorClass.prototype.bindTemporaryEvents.call(this, td, row, col, prop, value, cellProperties); - -}; - -HandsontableAutocompleteEditorClass.prototype.beginEditing = function () { - HandsontableTextEditorClass.prototype.beginEditing.apply(this, arguments); - - var that = this; - - this.instance.registerTimeout('IE9_align_fix', function () { //otherwise is misaligned in IE9 - that.typeahead.lookup(); - }, 1); -}; -/** - * @see HandsontableTextEditorClass.prototype.finishEditing - */ -HandsontableAutocompleteEditorClass.prototype.finishEditing = function (isCancelled, ctrlDown) { - if (!isCancelled) { - if (this.isMenuExpanded()) { - if(this.typeahead.$menu[0].querySelector('.active')){ - this.typeahead.select(); - this.state = this.STATE_FINISHED; - } else if (this.cellProperties.strict) { - this.state = this.STATE_FINISHED; - } - } - } - - HandsontableTextEditorClass.prototype.finishEditing.call(this, isCancelled, ctrlDown); -}; - -HandsontableAutocompleteEditorClass.prototype.isMenuExpanded = function () { - if (this.instance.view.wt.wtDom.isVisible(this.typeahead.$menu[0])) { - return this.typeahead; - } - else { - return false; - } -}; - -/** - * Autocomplete editor - * @param {Object} instance Handsontable instance - * @param {Element} td Table cell where to render - * @param {Number} row - * @param {Number} col - * @param {String|Number} prop Row object property name - * @param value Original value (remember to escape unsafe HTML before inserting to DOM!) - * @param {Object} cellProperties Cell properites (shared by cell renderer and editor) - */ -Handsontable.AutocompleteEditor = function (instance, td, row, col, prop, value, cellProperties) { - if (!instance.autocompleteEditor) { - instance.autocompleteEditor = new HandsontableAutocompleteEditorClass(instance); - } - instance.autocompleteEditor.bindTemporaryEvents(td, row, col, prop, value, cellProperties); - return function (isCancelled) { -// var isCancelled = true; - instance.autocompleteEditor.finishEditing(isCancelled); - } -}; -function toggleCheckboxCell(instance, row, prop, cellProperties) { - if (Handsontable.helper.stringify(instance.getDataAtRowProp(row, prop)) === Handsontable.helper.stringify(cellProperties.checkedTemplate)) { - instance.setDataAtRowProp(row, prop, cellProperties.uncheckedTemplate); - } - else { - instance.setDataAtRowProp(row, prop, cellProperties.checkedTemplate); - } -} - -/** - * Checkbox editor - * @param {Object} instance Handsontable instance - * @param {Element} td Table cell where to render - * @param {Number} row - * @param {Number} col - * @param {String|Number} prop Row object property name - * @param value Original value (remember to escape unsafe HTML before inserting to DOM!) - * @param {Object} cellProperties Cell properites (shared by cell renderer and editor) - */ -Handsontable.CheckboxEditor = function (instance, td, row, col, prop, value, cellProperties) { - if (typeof cellProperties === "undefined") { - cellProperties = {}; - } - if (typeof cellProperties.checkedTemplate === "undefined") { - cellProperties.checkedTemplate = true; - } - if (typeof cellProperties.uncheckedTemplate === "undefined") { - cellProperties.uncheckedTemplate = false; - } - - instance.$table.on("keydown.editor", function (event) { - var ctrlDown = (event.ctrlKey || event.metaKey) && !event.altKey; //catch CTRL but not right ALT (which in some systems triggers ALT+CTRL) - if (!ctrlDown && Handsontable.helper.isPrintableChar(event.keyCode)) { - toggleCheckboxCell(instance, row, prop, cellProperties); - event.stopImmediatePropagation(); //stops core onKeyDown handler - event.preventDefault(); //some keys have special behavior, eg. space bar scrolls screen down - } - }); - - instance.view.wt.update('onCellDblClick', function () { - toggleCheckboxCell(instance, row, prop, cellProperties); - }); - - return function () { - instance.$table.off(".editor"); - instance.view.wt.update('onCellDblClick', null); - } -}; - - - -function HandsontableDateEditorClass(instance) { - if (!$.datepicker) { - throw new Error("jQuery UI Datepicker dependency not found. Did you forget to include jquery-ui.custom.js or its substitute?"); - } - - this.isCellEdited = false; - this.instance = instance; - var that = this; - this.createElements(); - this.bindEvents(); - - this.instance.addHook('afterDestroy', function(){ - that.destroyElements(); - }) -} - -Handsontable.helper.inherit(HandsontableDateEditorClass, HandsontableTextEditorClass); - -/** - * @see HandsontableTextEditorClass.prototype.createElements - */ -HandsontableDateEditorClass.prototype.createElements = function () { - HandsontableTextEditorClass.prototype.createElements.call(this); - - this.datePicker = document.createElement('DIV'); - this.instance.view.wt.wtDom.addClass(this.datePicker, 'htDatepickerHolder'); - this.datePickerStyle = this.datePicker.style; - this.datePickerStyle.position = 'absolute'; - this.datePickerStyle.top = 0; - this.datePickerStyle.left = 0; - this.datePickerStyle.zIndex = 99; - document.body.appendChild(this.datePicker); - this.$datePicker = $(this.datePicker); - - var that = this; - var defaultOptions = { - dateFormat: "yy-mm-dd", - showButtonPanel: true, - changeMonth: true, - changeYear: true, - altField: this.$textarea, - onSelect: function () { - that.finishEditing(false); - } - }; - this.$datePicker.datepicker(defaultOptions); - - /** - * Prevent recognizing clicking on jQuery Datepicker as clicking outside of table - */ - this.$datePicker.on('mousedown', function(event){ - event.stopPropagation(); - }); - - this.hideDatepicker(); -}; - -HandsontableDateEditorClass.prototype.destroyElements = function(){ - this.$datePicker.datepicker('destroy'); - this.$datePicker.remove(); -}; - -/** - * @see HandsontableTextEditorClass.prototype.beginEditing - */ -HandsontableDateEditorClass.prototype.beginEditing = function (row, col, prop, useOriginalValue, suffix) { - HandsontableTextEditorClass.prototype.beginEditing.call(this, row, col, prop, useOriginalValue, suffix); - this.showDatepicker(); -}; - -/** - * @see HandsontableTextEditorClass.prototype.finishEditing - */ -HandsontableDateEditorClass.prototype.finishEditing = function (isCancelled, ctrlDown) { - this.hideDatepicker(); - HandsontableTextEditorClass.prototype.finishEditing.call(this, isCancelled, ctrlDown); -}; - -HandsontableDateEditorClass.prototype.showDatepicker = function () { - var $td = $(this.instance.dateEditor.TD); - var offset = $td.offset(); - this.datePickerStyle.top = (offset.top + $td.height()) + 'px'; - this.datePickerStyle.left = offset.left + 'px'; - - var dateOptions = { - defaultDate: this.originalValue || void 0 - }; - $.extend(dateOptions, this.cellProperties); - this.$datePicker.datepicker("option", dateOptions); - if (this.originalValue) { - this.$datePicker.datepicker("setDate", this.originalValue); - } - this.datePickerStyle.display = 'block'; -}; - -HandsontableDateEditorClass.prototype.hideDatepicker = function () { - this.datePickerStyle.display = 'none'; -}; - -/** - * Date editor (uses jQuery UI Datepicker) - * @param {Object} instance Handsontable instance - * @param {Element} td Table cell where to render - * @param {Number} row - * @param {Number} col - * @param {String|Number} prop Row object property name - * @param value Original value (remember to escape unsafe HTML before inserting to DOM!) - * @param {Object} cellProperties Cell properites (shared by cell renderer and editor) - */ -Handsontable.DateEditor = function (instance, td, row, col, prop, value, cellProperties) { - if (!instance.dateEditor) { - instance.dateEditor = new HandsontableDateEditorClass(instance); - } - instance.dateEditor.bindTemporaryEvents(td, row, col, prop, value, cellProperties); - return function (isCancelled) { - instance.dateEditor.finishEditing(isCancelled); - } -}; -/** - * This is inception. Using Handsontable as Handsontable editor - */ - -function HandsontableHandsontableEditorClass(instance) { - this.instance = instance; - this.createElements(); - this.bindEvents(); -} - -Handsontable.helper.inherit(HandsontableHandsontableEditorClass, HandsontableTextEditorClass); - -HandsontableHandsontableEditorClass.prototype.createElements = function () { - HandsontableTextEditorClass.prototype.createElements.call(this); - - var DIV = document.createElement('DIV'); - DIV.className = 'handsontableEditor'; - this.TEXTAREA_PARENT.appendChild(DIV); - - this.$htContainer = $(DIV); -}; - -HandsontableHandsontableEditorClass.prototype.bindTemporaryEvents = function (td, row, col, prop, value, cellProperties) { - var parent = this; - - var options = { - colHeaders: true, - cells: function () { - return { - readOnly: true - } - }, - fillHandle: false, - width: 2000, - //width: 'auto', - afterOnCellMouseDown: function () { - var sel = this.getSelected(); - parent.TEXTAREA.value = this.getDataAtCell(sel[0], sel[1]); - parent.instance.destroyEditor(); - }, - beforeOnKeyDown: function (event) { - switch (event.keyCode) { - case 27: //esc - parent.instance.destroyEditor(true); - break; - - case 13: //enter - var sel = this.getSelected(); - parent.TEXTAREA.value = this.getDataAtCell(sel[0], sel[1]); - parent.instance.destroyEditor(); - break; - } - } - }; - - if (cellProperties.handsontable) { - options = $.extend(options, cellProperties.handsontable); - } - - this.$htContainer.handsontable(options); - - HandsontableTextEditorClass.prototype.bindTemporaryEvents.call(this, td, row, col, prop, value, cellProperties); -}; - -HandsontableHandsontableEditorClass.prototype.beginEditing = function (row, col, prop, useOriginalValue, suffix) { - var onBeginEditing = this.instance.getSettings().onBeginEditing; - if (onBeginEditing && onBeginEditing() === false) { - return; - } - - HandsontableTextEditorClass.prototype.beginEditing.call(this, row, col, prop, useOriginalValue, suffix); - - this.$htContainer.handsontable('render'); - this.$htContainer.handsontable('selectCell', 0, 0); -}; - -HandsontableHandsontableEditorClass.prototype.finishEditing = function (isCancelled, ctrlDown) { - if (this.$htContainer.handsontable('isListening')) { //if focus is still in the HOT editor - this.instance.listen(); //return the focus to the parent HOT instance - } - this.$htContainer.handsontable('destroy'); - HandsontableTextEditorClass.prototype.finishEditing.call(this, isCancelled, ctrlDown); -}; - -/** - * Handsontable editor - * @param {Object} instance Handsontable instance - * @param {Element} td Table cell where to render - * @param {Number} row - * @param {Number} col - * @param {String|Number} prop Row object property name - * @param value Original value (remember to escape unsafe HTML before inserting to DOM!) - * @param {Object} cellProperties Cell properites (shared by cell renderer and editor) - */ -Handsontable.HandsontableEditor = function (instance, td, row, col, prop, value, cellProperties) { - if (!instance.handsontableEditor) { - instance.handsontableEditor = new HandsontableHandsontableEditorClass(instance); - } - instance.handsontableEditor.bindTemporaryEvents(td, row, col, prop, value, cellProperties); - - instance.registerEditor = instance.handsontableEditor; - - return function (isCancelled) { - instance.handsontableEditor.finishEditing(isCancelled); - } -}; - -(function(Handsontable){ - function HandsontablePasswordEditorClass(instance){ - HandsontableTextEditorClass.call(this, instance); - } - - Handsontable.helper.inherit(HandsontablePasswordEditorClass, HandsontableTextEditorClass); - - HandsontablePasswordEditorClass.prototype.createElements = function(){ - this.STATE_VIRGIN = 'STATE_VIRGIN'; //before editing - this.STATE_EDITING = 'STATE_EDITING'; - this.STATE_WAITING = 'STATE_WAITING'; //waiting for async validation - this.STATE_FINISHED = 'STATE_FINISHED'; - - this.state = this.STATE_VIRGIN; - this.waitingEvent = null; - - this.wtDom = new WalkontableDom(); - - this.TEXTAREA = document.createElement('input'); - this.TEXTAREA.setAttribute('type', 'password'); - this.TEXTAREA.className = 'handsontableInput'; - this.textareaStyle = this.TEXTAREA.style; - this.textareaStyle.width = 0; - this.textareaStyle.height = 0; - this.$textarea = $(this.TEXTAREA); - - this.TEXTAREA_PARENT = document.createElement('DIV'); - this.TEXTAREA_PARENT.className = 'handsontableInputHolder'; - this.textareaParentStyle = this.TEXTAREA_PARENT.style; - this.textareaParentStyle.top = 0; - this.textareaParentStyle.left = 0; - this.textareaParentStyle.display = 'none'; - - this.$body = $(document.body); - - this.TEXTAREA_PARENT.appendChild(this.TEXTAREA); - this.instance.rootElement[0].appendChild(this.TEXTAREA_PARENT); - }; - - Handsontable.PasswordEditor = function (instance, td, row, col, prop, value, cellProperties) { - if (!instance.passwordEditor) { - instance.passwordEditor = new HandsontablePasswordEditorClass(instance); - } - if (instance.passwordEditor.state === instance.passwordEditor.STATE_VIRGIN || instance.passwordEditor.state === instance.passwordEditor.STATE_FINISHED) { - instance.passwordEditor.bindTemporaryEvents(td, row, col, prop, value, cellProperties); - } - return function (isCancelled) { - instance.passwordEditor.finishEditing(isCancelled); - } - }; - -})(Handsontable); - -/** - * Numeric cell validator - * @param {*} value - Value of edited cell - * @param {*} calback - Callback called with validation result - */ -Handsontable.NumericValidator = function (value, callback) { - callback(/^-?\d*\.?\d*$/.test(value)); -} -/** - * Function responsible for validation of autocomplete value - * @param {*} value - Value of edited cell - * @param {*} calback - Callback called with validation result - */ -var process = function (value, callback) { - - var originalVal = value; - var lowercaseVal = typeof originalVal === 'string' ? originalVal.toLowerCase() : null; - - return function (source) { - var found = false; - for (var s = 0, slen = source.length; s < slen; s++) { - if (originalVal === source[s]) { - found = true; //perfect match - break; - } - else if (lowercaseVal === source[s].toLowerCase()) { - // changes[i][3] = source[s]; //good match, fix the case << TODO? - found = true; - break; - } - } - - callback(found); - } -}; - -/** - * Autocomplete cell validator - * @param {*} value - Value of edited cell - * @param {*} calback - Callback called with validation result - */ -Handsontable.AutocompleteValidator = function (value, callback) { - if (this.strict && this.source) { - $.isFunction(this.source) ? this.source(value, process(value, callback)) : process(value, callback)(this.source); - } else { - callback(true); - } -} - -/** - * Cell type is just a shortcut for setting bunch of cellProperties (used in getCellMeta) - */ - -Handsontable.AutocompleteCell = { - editor: Handsontable.AutocompleteEditor, - renderer: Handsontable.AutocompleteRenderer, - validator: Handsontable.AutocompleteValidator -}; - -Handsontable.CheckboxCell = { - editor: Handsontable.CheckboxEditor, - renderer: Handsontable.CheckboxRenderer -}; - -Handsontable.TextCell = { - editor: Handsontable.TextEditor, - renderer: Handsontable.TextRenderer -}; - -Handsontable.NumericCell = { - editor: Handsontable.TextEditor, - renderer: Handsontable.NumericRenderer, - validator: Handsontable.NumericValidator, - dataType: 'number' -}; - -Handsontable.DateCell = { - editor: Handsontable.DateEditor, - renderer: Handsontable.AutocompleteRenderer //displays small gray arrow on right side of the cell -}; - -Handsontable.HandsontableCell = { - editor: Handsontable.HandsontableEditor, - renderer: Handsontable.AutocompleteRenderer //displays small gray arrow on right side of the cell -}; - -Handsontable.PasswordCell = { - editor: Handsontable.PasswordEditor, - renderer: Handsontable.PasswordRenderer -}; - -//here setup the friendly aliases that are used by cellProperties.type -Handsontable.cellTypes = { - text: Handsontable.TextCell, - date: Handsontable.DateCell, - numeric: Handsontable.NumericCell, - checkbox: Handsontable.CheckboxCell, - autocomplete: Handsontable.AutocompleteCell, - handsontable: Handsontable.HandsontableCell, - password: Handsontable.PasswordCell -}; - -//here setup the friendly aliases that are used by cellProperties.renderer and cellProperties.editor -Handsontable.cellLookup = { - renderer: { - text: Handsontable.TextRenderer, - numeric: Handsontable.NumericRenderer, - checkbox: Handsontable.CheckboxRenderer, - autocomplete: Handsontable.AutocompleteRenderer, - password: Handsontable.PasswordRenderer - }, - editor: { - text: Handsontable.TextEditor, - date: Handsontable.DateEditor, - checkbox: Handsontable.CheckboxEditor, - autocomplete: Handsontable.AutocompleteEditor, - handsontable: Handsontable.HandsontableEditor, - password: Handsontable.PasswordEditor - }, - validator: { - numeric: Handsontable.NumericValidator, - autocomplete: Handsontable.AutocompleteValidator - } -}; -Handsontable.PluginHookClass = (function () { - - var Hooks = function () { - return { - // Hooks - beforeInitWalkontable: [], - - beforeInit: [], - beforeRender: [], - beforeChange: [], - beforeRemoveCol: [], - beforeRemoveRow: [], - beforeValidate: [], - beforeGet: [], - beforeSet: [], - beforeGetCellMeta: [], - beforeAutofill: [], - beforeKeyDown: [], - - afterInit : [], - afterLoadData : [], - afterUpdateSettings: [], - afterRender : [], - afterRenderer : [], - afterChange : [], - afterValidate: [], - afterGetCellMeta: [], - afterGetColHeader: [], - afterGetColWidth: [], - afterDestroy: [], - afterRemoveRow: [], - afterCreateRow: [], - afterRemoveCol: [], - afterCreateCol: [], - afterColumnResize: [], - afterColumnMove: [], - afterDeselect: [], - afterSelection: [], - afterSelectionByProp: [], - afterSelectionEnd: [], - afterSelectionEndByProp: [], - afterCopyLimit: [], - - // Modifiers - modifyCol: [] - } - }; - - var legacy = { - onBeforeChange: "beforeChange", - onChange: "afterChange", - onCreateRow: "afterCreateRow", - onCreateCol: "afterCreateCol", - onSelection: "afterSelection", - onCopyLimit: "afterCopyLimit", - onSelectionEnd: "afterSelectionEnd", - onSelectionByProp: "afterSelectionByProp", - onSelectionEndByProp: "afterSelectionEndByProp" - }; - - function PluginHookClass() { - - this.hooks = { - once: Hooks(), - persistent: Hooks() - }; - - this.legacy = legacy; - - } - - var addHook = function (type) { - return function (key, fn) { - // provide support for old versions of HOT - if (key in legacy) { - key = legacy[key]; - } - - if (typeof this.hooks[type][key] === "undefined") { - this.hooks[type][key] = []; - } - - if (fn instanceof Array) { - for (var i = 0, len = fn.length; i < len; i++) { - this.hooks[type][key].push(fn[i]); - } - } else { - this.hooks[type][key].push(fn); - } - - return this; - }; - }; - - PluginHookClass.prototype.add = addHook('persistent'); - PluginHookClass.prototype.once = addHook('once'); - - PluginHookClass.prototype.remove = function (key, fn) { - var status = false - , hookTypes = ['persistent', 'once'] - , type, x, lenx, i, leni; - - // provide support for old versions of HOT - if (key in legacy) { - key = legacy[key]; - } - - for (x = 0, lenx = hookTypes.length; x < lenx; x++) { - type = hookTypes[x]; - if (typeof this.hooks[type][key] !== 'undefined') { - - for (i = 0, leni = this.hooks[type][key].length; i < leni; i++) { - if (this.hooks[type][key][i] == fn) { - this.hooks[type][key].splice(i, 1); - status = true; - break; - } - } - - } - } - - return status; - }; - - PluginHookClass.prototype.run = function (instance, key, p1, p2, p3, p4, p5) { - var hookTypes = ['persistent', 'once'] - , type, x, lenx, i, leni; - - // provide support for old versions of HOT - if (key in legacy) { - key = legacy[key]; - } - - //performance considerations - http://jsperf.com/call-vs-apply-for-a-plugin-architecture - for (x = 0, lenx = hookTypes.length; x < lenx; x++) { - type = hookTypes[x]; - if (typeof this.hooks[type][key] !== 'undefined') { - - for (i = 0, leni = this.hooks[type][key].length; i < leni; i++) { - this.hooks[type][key][i].call(instance, p1, p2, p3, p4, p5); - - if (type === 'once') { - this.hooks[type][key].splice(i, 1); - leni--; - i--; - } - } - - } - } - }; - - PluginHookClass.prototype.execute = function (instance, key, p1, p2, p3, p4, p5) { - var hookTypes = ['persistent', 'once'] - , type, x, lenx, i, leni, res; - - // provide support for old versions of HOT - if (key in legacy) { - key = legacy[key]; - } - - //performance considerations - http://jsperf.com/call-vs-apply-for-a-plugin-architecture - for (x = 0, lenx = hookTypes.length; x < lenx; x++) { - type = hookTypes[x]; - if (typeof this.hooks[type][key] !== 'undefined') { - - for (i = 0, leni = this.hooks[type][key].length; i < leni; i++) { - - res = this.hooks[type][key][i].call(instance, p1, p2, p3, p4, p5); - if (res !== void 0) { - p1 = res; - } - - if (type === 'once') { - this.hooks[type][key].splice(i, 1); - leni--; - i--; - } - } - - } - } - - return p1; - }; - - return PluginHookClass; - -})(); - -Handsontable.PluginHooks = new Handsontable.PluginHookClass(); - -(function (Handsontable) { - - function HandsontableAutoColumnSize() { - var plugin = this - , sampleCount = 5; //number of samples to take of each value length - - this.beforeInit = function () { - var instance = this; - instance.autoColumnWidths = []; - - if (instance.getSettings().autoColumnSize !== false) { - - if (!instance.autoColumnSizeTmp) { - instance.autoColumnSizeTmp = { - table: null, - tableStyle: null, - theadTh: null, - tbody: null, - container: null, - containerStyle: null, - determineBeforeNextRender: true - }; - } - - instance.addHook('beforeRender', htAutoColumnSize.determineIfChanged); - instance.addHook('afterGetColWidth', htAutoColumnSize.getColWidth); - instance.addHook('afterDestroy', htAutoColumnSize.afterDestroy); - - instance.determineColumnWidth = plugin.determineColumnWidth; - } else { - instance.removeHook('beforeRender', htAutoColumnSize.determineIfChanged); - instance.removeHook('afterGetColWidth', htAutoColumnSize.getColWidth); - instance.removeHook('afterDestroy', htAutoColumnSize.afterDestroy); - - delete instance.determineColumnWidth; - - plugin.afterDestroy.call(instance); - - } - - }; - - this.determineIfChanged = function (force) { - if (force) { - htAutoColumnSize.determineColumnsWidth.apply(this, arguments); - } - }; - - this.determineColumnWidth = function (col) { - var instance = this - , tmp = instance.autoColumnSizeTmp; - - if (!tmp.container) { - createTmpContainer.call(tmp, instance); - } - - tmp.container.className = instance.rootElement[0].className + ' htAutoColumnSize'; - tmp.table.className = instance.$table[0].className; - - var rows = instance.countRows(); - var samples = {}; - var maxLen = 0; - for (var r = 0; r < rows; r++) { - var value = Handsontable.helper.stringify(instance.getDataAtCell(r, col)); - var len = value.length; - if (len > maxLen) { - maxLen = len; - } - if (!samples[len]) { - samples[len] = { - needed: sampleCount, - strings: [] - }; - } - if (samples[len].needed) { - samples[len].strings.push({value: value, row: r}); - samples[len].needed--; - } - } - - var settings = instance.getSettings(); - if (settings.colHeaders) { - instance.view.appendColHeader(col, tmp.theadTh); //TH innerHTML - } - - instance.view.wt.wtDom.empty(tmp.tbody); - - var cellProperties = instance.getCellMeta(0, col); - var renderer = Handsontable.helper.getCellMethod('renderer', cellProperties.renderer); - - for (var i in samples) { - if (samples.hasOwnProperty(i)) { - for (var j = 0, jlen = samples[i].strings.length; j < jlen; j++) { - var tr = document.createElement('tr'); - var td = document.createElement('td'); - renderer(instance, td, samples[i].strings[j].row, col, instance.colToProp(col), samples[i].strings[j].value, cellProperties); - r++; - tr.appendChild(td); - tmp.tbody.appendChild(tr); - } - } - } - - var parent = instance.rootElement[0].parentNode; - parent.appendChild(tmp.container); - var width = instance.view.wt.wtDom.outerWidth(tmp.table); - parent.removeChild(tmp.container); - - var maxWidth = instance.view.wt.wtViewport.getViewportWidth() - 2; //2 is some overhead for cell border - if (width > maxWidth) { - width = maxWidth; - } - - return width; - }; - - this.determineColumnsWidth = function () { - var instance = this; - var settings = this.getSettings(); - if (settings.autoColumnSize || !settings.colWidths) { - var cols = this.countCols(); - for (var c = 0; c < cols; c++) { - if (!instance._getColWidthFromSettings(c)) { - this.autoColumnWidths[c] = plugin.determineColumnWidth.call(instance, c); - } - } - } - }; - - this.getColWidth = function (col, response) { - if (this.autoColumnWidths[col] && this.autoColumnWidths[col] > response.width) { - response.width = this.autoColumnWidths[col]; - } - }; - - this.afterDestroy = function () { - var instance = this; - if (instance.autoColumnSizeTmp && instance.autoColumnSizeTmp.container && instance.autoColumnSizeTmp.container.parentNode) { - instance.autoColumnSizeTmp.container.parentNode.removeChild(instance.autoColumnSizeTmp.container); - } - }; - - function createTmpContainer(instance) { - var d = document - , tmp = this; - - tmp.table = d.createElement('table'); - tmp.theadTh = d.createElement('th'); - tmp.table.appendChild(d.createElement('thead')).appendChild(d.createElement('tr')).appendChild(tmp.theadTh); - - tmp.tableStyle = tmp.table.style; - tmp.tableStyle.tableLayout = 'auto'; - tmp.tableStyle.width = 'auto'; - - tmp.tbody = d.createElement('tbody'); - tmp.table.appendChild(tmp.tbody); - - tmp.container = d.createElement('div'); - tmp.container.className = instance.rootElement[0].className + ' hidden'; - tmp.containerStyle = tmp.container.style; - - tmp.container.appendChild(tmp.table); - } - } - - var htAutoColumnSize = new HandsontableAutoColumnSize(); - - Handsontable.PluginHooks.add('beforeInit', htAutoColumnSize.beforeInit); - Handsontable.PluginHooks.add('afterUpdateSettings', htAutoColumnSize.beforeInit); - -})(Handsontable); - -/** - * This plugin sorts the view by a column (but does not sort the data source!) - * @constructor - */ -function HandsontableColumnSorting() { - var plugin = this; - - this.init = function (source) { - var instance = this; - var sortingSettings = instance.getSettings().columnSorting; - var sortingColumn, sortingOrder; - - instance.sortingEnabled = !!(sortingSettings); - - if (instance.sortingEnabled) { - instance.sortIndex = []; - - var loadedSortingState = loadSortingState.call(instance); - - if (typeof loadedSortingState != 'undefined') { - sortingColumn = loadedSortingState.sortColumn; - sortingOrder = loadedSortingState.sortOrder; - } else { - sortingColumn = sortingSettings.column; - sortingOrder = sortingSettings.sortOrder; - } - plugin.sortByColumn.call(instance, sortingColumn, sortingOrder); - - instance.sort = function(){ - var args = Array.prototype.slice.call(arguments); - - return plugin.sortByColumn.apply(instance, args) - } - - if (typeof instance.getSettings().observeChanges == 'undefined'){ - enableObserveChangesPlugin.call(instance); - } - - if (source == 'afterInit') { - bindColumnSortingAfterClick.call(instance); - - instance.addHook('afterCreateRow', plugin.afterCreateRow); - instance.addHook('afterRemoveRow', plugin.afterRemoveRow); - } - } else { - delete instance.sort; - } - }; - - this.setSortingColumn = function (col, order) { - var instance = this; - - if (typeof col == 'undefined') { - delete instance.sortColumn; - delete instance.sortOrder; - - return; - } else if (instance.sortColumn === col && typeof order == 'undefined') { - instance.sortOrder = !instance.sortOrder; - } else { - instance.sortOrder = typeof order != 'undefined' ? order : true; - } - - instance.sortColumn = col; - - }; - - this.sortByColumn = function (col, order) { - var instance = this; - - plugin.setSortingColumn.call(instance, col, order); - - instance.PluginHooks.run('beforeColumnSort', instance.sortColumn, instance.sortOrder); - - plugin.sort.call(instance); - instance.render(); - - saveSortingState.call(instance); - - instance.PluginHooks.run('afterColumnSort', instance.sortColumn, instance.sortOrder); - }; - - var saveSortingState = function () { - var instance = this; - - var sortingState = {}; - - if (typeof instance.sortColumn != 'undefined') { - sortingState.sortColumn = instance.sortColumn; - } - - if (typeof instance.sortOrder != 'undefined') { - sortingState.sortOrder = instance.sortOrder; - } - - if (sortingState.hasOwnProperty('sortColumn') || sortingState.hasOwnProperty('sortOrder')) { - instance.PluginHooks.run('persistentStateSave', 'columnSorting', sortingState); - } - - }; - - var loadSortingState = function () { - var instance = this; - var storedState = {}; - instance.PluginHooks.run('persistentStateLoad', 'columnSorting', storedState); - - return storedState.value; - }; - - var bindColumnSortingAfterClick = function () { - var instance = this; - - instance.rootElement.on('click.handsontable', '.columnSorting', function (e) { - if (instance.view.wt.wtDom.hasClass(e.target, 'columnSorting')) { - var col = getColumn(e.target); - plugin.sortByColumn.call(instance, col); - } - }); - - function countRowHeaders() { - var THs = instance.view.TBODY.querySelector('tr').querySelectorAll('th'); - return THs.length; - } - - function getColumn(target) { - var TH = instance.view.wt.wtDom.closest(target, 'TH'); - return instance.view.wt.wtDom.index(TH) - countRowHeaders(); - } - }; - - function enableObserveChangesPlugin () { - var instance = this; - instance.registerTimeout('enableObserveChanges', function(){ - instance.updateSettings({ - observeChanges: true - }); - }, 0); - } - - function defaultSort(sortOrder) { - return function (a, b) { - if (a[1] === b[1]) { - return 0; - } - if (a[1] === null) { - return 1; - } - if (b[1] === null) { - return -1; - } - if (a[1] < b[1]) return sortOrder ? -1 : 1; - if (a[1] > b[1]) return sortOrder ? 1 : -1; - return 0; - } - } - - function dateSort(sortOrder) { - return function (a, b) { - if (a[1] === b[1]) { - return 0; - } - if (a[1] === null) { - return 1; - } - if (b[1] === null) { - return -1; - } - - var aDate = new Date(a[1]); - var bDate = new Date(b[1]); - - if (aDate < bDate) return sortOrder ? -1 : 1; - if (aDate > bDate) return sortOrder ? 1 : -1; - - return 0; - } - } - - this.sort = function () { - var instance = this; - - if (typeof instance.sortOrder == 'undefined') { - return; - } - - instance.sortingEnabled = false; //this is required by translateRow plugin hook - instance.sortIndex.length = 0; - - var colOffset = this.colOffset(); - for (var i = 0, ilen = this.countRows() - instance.getSettings()['minSpareRows']; i < ilen; i++) { - this.sortIndex.push([i, instance.getDataAtCell(i, this.sortColumn + colOffset)]); - } - - var colMeta = instance.getCellMeta(0, instance.sortColumn); - var sortFunction; - switch (colMeta.type) { - case 'date': - sortFunction = dateSort; - break; - default: - sortFunction = defaultSort; - } - - this.sortIndex.sort(sortFunction(instance.sortOrder)); - - //Append spareRows - for(var i = this.sortIndex.length; i < instance.countRows(); i++){ - this.sortIndex.push([i, instance.getDataAtCell(i, this.sortColumn + colOffset)]); - } - - instance.sortingEnabled = true; //this is required by translateRow plugin hook - }; - - this.translateRow = function (row) { - var instance = this; - if (instance.sortingEnabled && instance.sortIndex && instance.sortIndex.length) { - return instance.sortIndex[row][0]; - } - return row; - }; - - this.onBeforeGetSet = function (getVars) { - var instance = this; - getVars.row = plugin.translateRow.call(instance, getVars.row); - }; - - this.untranslateRow = function (row) { - if (sortingEnabled && this.sortIndex && this.sortIndex.length) { - for (var i = 0; i < this.sortIndex.length; i++) { - if (this.sortIndex[i][0] == row) { - return i; - } - } - } - }; - - this.getColHeader = function (col, TH) { - if (this.getSettings().columnSorting) { - this.view.wt.wtDom.addClass(TH.querySelector('.colHeader'), 'columnSorting'); - } - }; - - function isSorted(instance){ - return typeof instance.sortColumn != 'undefined'; - } - - this.afterCreateRow = function(index, amount){ - var instance = this; - - if(!isSorted(instance)){ - return; - } - - instance.sortIndex.splice(index, 0, [index, instance.getData()[index][this.sortColumn + instance.colOffset()]]); - - for(var i = 0; i < instance.sortIndex.length; i++){ - if(i == index) continue; - - if (instance.sortIndex[i][0] >= index){ - instance.sortIndex[i][0] += 1; - } - } - - saveSortingState.call(instance); - - }; - - this.afterRemoveRow = function(index, amount){ - var instance = this; - - if(!isSorted(instance)){ - return; - } - - instance.sortIndex.splice(index, amount); - - for(var i = 0; i < instance.sortIndex.length; i++){ - - if (instance.sortIndex[i][0] > index){ - instance.sortIndex[i][0] -= amount; - } - } - - saveSortingState.call(instance); - - }; - - this.afterChangeSort = function (changes/*, source*/) { - var instance = this; - var sortColumnChanged = false; - var selection = {}; - if (!changes) { - return; - } - - for (var i = 0; i < changes.length; i++) { - if (changes[i][1] == instance.sortColumn) { - sortColumnChanged = true; - selection.row = plugin.translateRow.call(instance, changes[i][0]); - selection.col = changes[i][1]; - break; - } - } - - if (sortColumnChanged) { - setTimeout(function () { - plugin.sort.call(instance); - instance.render(); - instance.selectCell(plugin.untranslateRow.call(instance, selection.row), selection.col); - }, 0); - } - }; -} -var htSortColumn = new HandsontableColumnSorting(); - -Handsontable.PluginHooks.add('afterInit', function () { - htSortColumn.init.call(this, 'afterInit') -}); -Handsontable.PluginHooks.add('afterUpdateSettings', function () { - htSortColumn.init.call(this, 'afterUpdateSettings') -}); -Handsontable.PluginHooks.add('beforeGet', htSortColumn.onBeforeGetSet); -Handsontable.PluginHooks.add('beforeSet', htSortColumn.onBeforeGetSet); -Handsontable.PluginHooks.add('afterGetColHeader', htSortColumn.getColHeader); - - -(function(Handsontable){ - function init(){ - var instance = this; - var pluginEnabled = !!(instance.getSettings().contextMenu); - - if(pluginEnabled){ - createContextMenu.call(instance); - } else { - destroyContextMenu.call(instance); - } - } - - function createContextMenu() { - var instance = this - , selectorId = instance.rootElement[0].id - , allItems = { - "row_above": {name: "Insert row above", disabled: isDisabled}, - "row_below": {name: "Insert row below", disabled: isDisabled}, - "hsep1": "---------", - "col_left": {name: "Insert column on the left", disabled: isDisabled}, - "col_right": {name: "Insert column on the right", disabled: isDisabled}, - "hsep2": "---------", - "remove_row": {name: "Remove row", disabled: isDisabled}, - "remove_col": {name: "Remove column", disabled: isDisabled}, - "hsep3": "---------", - "undo": {name: "Undo", disabled: function () { - return !instance.undoRedo || !instance.isUndoAvailable(); - }}, - "redo": {name: "Redo", disabled: function () { - return !instance.undoRedo || !instance.isRedoAvailable(); - }} - } - , defaultOptions = { - selector : "#" + selectorId + ' table, #' + selectorId + ' div', - trigger : 'right', - callback : onContextClick - } - , options = {} - , i - , ilen - , settings = instance.getSettings(); - - function onContextClick(key) { - var corners = instance.getSelected(); //[selection start row, selection start col, selection end row, selection end col] - - if (!corners) { - return; //needed when there are 2 grids on a page - } - - /** - * `selection` variable contains normalized selection coordinates. - * selection.start - top left corner of selection area - * selection.end - bottom right corner of selection area - */ - - var selection = { - start: new Handsontable.SelectionPoint(), - end: new Handsontable.SelectionPoint() - }; - - selection.start.row(Math.min(corners[0], corners[2])); - selection.start.col(Math.min(corners[1], corners[3])); - - selection.end.row(Math.max(corners[0], corners[2])); - selection.end.col(Math.max(corners[1], corners[3])); - - switch (key) { - case "row_above": - instance.alter("insert_row", selection.start.row()); - break; - - case "row_below": - instance.alter("insert_row", selection.end.row() + 1); - break; - - case "col_left": - instance.alter("insert_col", selection.start.col()); - break; - - case "col_right": - instance.alter("insert_col", selection.end.col() + 1); - break; - - case "remove_row": - instance.alter(key, selection.start.row(), (selection.end.row() - selection.start.row()) + 1); - break; - - case "remove_col": - instance.alter(key, selection.start.col(), (selection.end.col() - selection.start.col()) + 1); - break; - - case "undo": - instance.undo(); - break; - - case "redo": - instance.redo(); - break; - } - } - - function isDisabled(key) { - //TODO rewrite - /*if (instance.blockedCols.main.find('th.htRowHeader.active').length && (key === "remove_col" || key === "col_left" || key === "col_right")) { - return true; - } - else if (instance.blockedRows.main.find('th.htColHeader.active').length && (key === "remove_row" || key === "row_above" || key === "row_below")) { - return true; - } - else*/ - if (instance.countRows() >= instance.getSettings().maxRows && (key === "row_above" || key === "row_below")) { - return true; - } - else if (instance.countCols() >= instance.getSettings().maxCols && (key === "col_left" || key === "col_right")) { - return true; - } - else { - return false; - } - } - - if (settings.contextMenu === true) { //contextMenu is true - options.items = allItems; - } - else if (Object.prototype.toString.apply(settings.contextMenu) === '[object Array]') { //contextMenu is an array - options.items = {}; - for (i = 0, ilen = settings.contextMenu.length; i < ilen; i++) { - var key = settings.contextMenu[i]; - if (typeof allItems[key] === 'undefined') { - throw new Error('Context menu key "' + key + '" is not recognised'); - } - options.items[key] = allItems[key]; - } - } - else if (Object.prototype.toString.apply(settings.contextMenu) === '[object Object]') { //contextMenu is an options object as defined in http://medialize.github.com/jQuery-contextMenu/docs.html - options = settings.contextMenu; - if (options.items) { - for (i in options.items) { - if (options.items.hasOwnProperty(i) && allItems[i]) { - if (typeof options.items[i] === 'string') { - options.items[i] = allItems[i]; - } - else { - options.items[i] = $.extend(true, allItems[i], options.items[i]); - } - } - } - } - else { - options.items = allItems; - } - - if (options.callback) { - var handsontableCallback = defaultOptions.callback; - var customCallback = options.callback; - options.callback = function (key, options) { - handsontableCallback(key, options); - customCallback(key, options); - } - } - } - - if (!selectorId) { - throw new Error("Handsontable container must have an id"); - } - - $.contextMenu($.extend(true, defaultOptions, options)); - } - - function destroyContextMenu() { - var id = this.rootElement[0].id; - $.contextMenu('destroy', "#" + id + ' table, #' + id + ' div'); - - /* - * There is a bug in $.contextMenu: 'destroy' does not remove layer when selector is provided. When the below line - * is removed, running the context menu tests in Jasmine will produce invisible layers that are never removed from DOM - */ - $(document.querySelectorAll('#context-menu-layer')).remove(); - } - - Handsontable.PluginHooks.add('afterInit', init); - Handsontable.PluginHooks.add('afterUpdateSettings', init); - Handsontable.PluginHooks.add('afterDestroy', destroyContextMenu); - -})(Handsontable); -/** - * This plugin adds support for legacy features, deprecated APIs, etc. - */ - -/** - * Support for old autocomplete syntax - * For old syntax, see: https://github.com/warpech/jquery-handsontable/blob/8c9e701d090ea4620fe08b6a1a048672fadf6c7e/README.md#defining-autocomplete - */ -Handsontable.PluginHooks.add('beforeGetCellMeta', function (row, col, cellProperties) { - var settings = this.getSettings(), data = this.getData(), i, ilen, a; - - //isWritable - deprecated since 0.8.0 - cellProperties.isWritable = !cellProperties.readOnly; - - //autocomplete - deprecated since 0.7.1 (see CHANGELOG.md) - if (settings.autoComplete) { - for (i = 0, ilen = settings.autoComplete.length; i < ilen; i++) { - if (settings.autoComplete[i].match(row, col, data)) { - if (typeof cellProperties.type === 'undefined') { - cellProperties.type = Handsontable.AutocompleteCell; - } - else { - if (typeof cellProperties.type.renderer === 'undefined') { - cellProperties.type.renderer = Handsontable.AutocompleteCell.renderer; - } - if (typeof cellProperties.type.editor === 'undefined') { - cellProperties.type.editor = Handsontable.AutocompleteCell.editor; - } - } - for (a in settings.autoComplete[i]) { - if (settings.autoComplete[i].hasOwnProperty(a) && a !== 'match' && typeof cellProperties[i] === 'undefined') { - if (a === 'source') { - cellProperties[a] = settings.autoComplete[i][a](row, col); - } - else { - cellProperties[a] = settings.autoComplete[i][a]; - } - } - } - break; - } - } - } -}); -function HandsontableManualColumnMove() { - var instance - , pressed - , startCol - , endCol - , startX - , startOffset; - - var ghost = document.createElement('DIV') - , ghostStyle = ghost.style; - - ghost.className = 'ghost'; - ghostStyle.position = 'absolute'; - ghostStyle.top = '25px'; - ghostStyle.left = 0; - ghostStyle.width = '10px'; - ghostStyle.height = '10px'; - ghostStyle.backgroundColor = '#CCC'; - ghostStyle.opacity = 0.7; - - var saveManualColumnPositions = function () { - var instance = this; - - instance.PluginHooks.run('persistentStateSave', 'manualColumnPositions', instance.manualColumnPositions); - }; - - var loadManualColumnPositions = function () { - var instance = this; - var storedState = {}; - instance.PluginHooks.run('persistentStateLoad', 'manualColumnPositions', storedState); - - return storedState.value; - }; - - - var bindMoveColEvents = function () { - var instance = this; - - instance.rootElement.on('mousemove.manualColumnMove', function (e) { - if (pressed) { - ghostStyle.left = startOffset + e.pageX - startX + 6 + 'px'; - if (ghostStyle.display === 'none') { - ghostStyle.display = 'block'; - } - } - }); - - instance.rootElement.on('mouseup.manualColumnMove', function () { - if (pressed) { - if (startCol < endCol) { - endCol--; - } - if (instance.getSettings().rowHeaders) { - startCol--; - endCol--; - } - instance.manualColumnPositions.splice(endCol, 0, instance.manualColumnPositions.splice(startCol, 1)[0]); - $('.manualColumnMover.active').removeClass('active'); - pressed = false; - instance.forceFullRender = true; - instance.view.render(); //updates all - ghostStyle.display = 'none'; - - saveManualColumnPositions.call(instance); - - instance.PluginHooks.run('afterColumnMove', startCol, endCol); - } - }); - - instance.rootElement.on('mousedown.manualColumnMove', '.manualColumnMover', function (e) { - - var mover = e.currentTarget; - var TH = instance.view.wt.wtDom.closest(mover, 'TH'); - startCol = instance.view.wt.wtDom.index(TH) + instance.colOffset(); - endCol = startCol; - pressed = true; - startX = e.pageX; - - var TABLE = instance.$table[0]; - TABLE.parentNode.appendChild(ghost); - ghostStyle.width = instance.view.wt.wtDom.outerWidth(TH) + 'px'; - ghostStyle.height = instance.view.wt.wtDom.outerHeight(TABLE) + 'px'; - startOffset = parseInt(instance.view.wt.wtDom.offset(TH).left - instance.view.wt.wtDom.offset(TABLE).left, 10); - ghostStyle.left = startOffset + 6 + 'px'; - }); - - instance.rootElement.on('mouseenter.manualColumnMove', 'td, th', function () { - if (pressed) { - var active = instance.view.THEAD.querySelector('.manualColumnMover.active'); - if (active) { - instance.view.wt.wtDom.removeClass(active, 'active'); - } - endCol = instance.view.wt.wtDom.index(this) + instance.colOffset(); - var THs = instance.view.THEAD.querySelectorAll('th'); - var mover = THs[endCol].querySelector('.manualColumnMover'); - instance.view.wt.wtDom.addClass(mover, 'active'); - } - }); - - instance.addHook('afterDestroy', unbindMoveColEvents); - }; - - var unbindMoveColEvents = function(){ - var instance = this; - instance.rootElement.off('mouseup.manualColumnMove'); - instance.rootElement.off('mousemove.manualColumnMove'); - instance.rootElement.off('mousedown.manualColumnMove'); - instance.rootElement.off('mouseenter.manualColumnMove'); - } - - this.beforeInit = function () { - this.manualColumnPositions = []; - }; - - this.init = function (source) { - var instance = this; - - var manualColMoveEnabled = !!(this.getSettings().manualColumnMove); - - if (manualColMoveEnabled) { - var initialManualColumnPositions = this.getSettings().manualColumnMove; - - var loadedManualColumnPositions = loadManualColumnPositions.call(instance); - - if (typeof loadedManualColumnPositions != 'undefined') { - this.manualColumnPositions = loadedManualColumnPositions; - } else if (initialManualColumnPositions instanceof Array) { - this.manualColumnPositions = initialManualColumnPositions; - } else { - this.manualColumnPositions = []; - } - - - instance.forceFullRender = true; - - if (source == 'afterInit') { - bindMoveColEvents.call(this); - if (this.manualColumnPositions.length > 0) { - this.forceFullRender = true; - this.render(); - } - - } - - } else { - unbindMoveColEvents.call(this); - this.manualColumnPositions = []; - } - }; - - this.modifyCol = function (col) { - //TODO test performance: http://jsperf.com/object-wrapper-vs-primitive/2 - if (this.getSettings().manualColumnMove) { - if (typeof this.manualColumnPositions[col] === 'undefined') { - this.manualColumnPositions[col] = col; - } - return this.manualColumnPositions[col]; - } - return col; - }; - - this.getColHeader = function (col, TH) { - if (this.getSettings().manualColumnMove) { - var DIV = document.createElement('DIV'); - DIV.className = 'manualColumnMover'; - TH.firstChild.appendChild(DIV); - } - }; -} -var htManualColumnMove = new HandsontableManualColumnMove(); - -Handsontable.PluginHooks.add('beforeInit', htManualColumnMove.beforeInit); -Handsontable.PluginHooks.add('afterInit', function () { - htManualColumnMove.init.call(this, 'afterInit') -}); - -Handsontable.PluginHooks.add('afterUpdateSettings', function () { - htManualColumnMove.init.call(this, 'afterUpdateSettings') -}); -Handsontable.PluginHooks.add('afterGetColHeader', htManualColumnMove.getColHeader); -Handsontable.PluginHooks.add('modifyCol', htManualColumnMove.modifyCol); - -function HandsontableManualColumnResize() { - var pressed - , currentTH - , currentCol - , currentWidth - , instance - , newSize - , startX - , startWidth - , startOffset - , resizer = document.createElement('DIV') - , handle = document.createElement('DIV') - , line = document.createElement('DIV') - , lineStyle = line.style; - - resizer.className = 'manualColumnResizer'; - - handle.className = 'manualColumnResizerHandle'; - resizer.appendChild(handle); - - line.className = 'manualColumnResizerLine'; - resizer.appendChild(line); - - var $document = $(document); - - $document.mousemove(function (e) { - if (pressed) { - currentWidth = startWidth + (e.pageX - startX); - newSize = setManualSize(currentCol, currentWidth); //save col width - resizer.style.left = startOffset + currentWidth + 'px'; - } - }); - - $document.mouseup(function () { - if (pressed) { - instance.view.wt.wtDom.removeClass(resizer, 'active'); - pressed = false; - - if(newSize != startWidth){ - instance.forceFullRender = true; - instance.view.render(); //updates all - - saveManualColumnWidths.call(instance); - - instance.PluginHooks.run('afterColumnResize', currentCol, newSize); - } - - refreshResizerPosition.call(instance, currentTH); - } - }); - - var saveManualColumnWidths = function () { - var instance = this; - - instance.PluginHooks.run('persistentStateSave', 'manualColumnWidths', instance.manualColumnWidths); - }; - - var loadManualColumnWidths = function () { - var instance = this; - var storedState = {}; - instance.PluginHooks.run('persistentStateLoad', 'manualColumnWidths', storedState); - - return storedState.value; - }; - - function refreshResizerPosition(TH) { - instance = this; - currentTH = TH; - - var col = this.view.wt.wtTable.getCoords(TH)[1]; //getCoords returns array [row, col] - if (col >= 0) { //if not row header - currentCol = col; - var rootOffset = this.view.wt.wtDom.offset(this.rootElement[0]).left; - var thOffset = this.view.wt.wtDom.offset(TH).left; - startOffset = (thOffset - rootOffset) - 6; - var thStyle = this.view.wt.wtDom.getComputedStyle(TH); - resizer.style.left = startOffset + parseInt(this.view.wt.wtDom.outerWidth(TH), 10) + 'px'; - - this.rootElement[0].appendChild(resizer); - } - } - - function getColumnWidth(TH) { - var instance = this; - var thOffset = instance.view.wt.wtDom.offset(TH).left - instance.view.wt.wtDom.offset(TH).left; - var rootOffset = instance.view.wt.wtDom.offset(instance.rootElement[0]).left; - var col = instance.view.wt.wtTable.getCoords(TH)[1]; //getCoords returns array [row, col] - var thWidth = instance.getColWidth(col); - var maxWidth = instance.view.maximumVisibleElementWidth(thOffset - rootOffset); - return Math.min(thWidth, maxWidth); - } - - function refreshLinePosition() { - var instance = this; - var thBorderWidth = 2 * parseInt(this.view.wt.wtDom.getComputedStyle(currentTH).borderWidth, 10); - startWidth = parseInt(this.view.wt.wtDom.outerWidth(currentTH), 10); - instance.view.wt.wtDom.addClass(resizer, 'active'); - lineStyle.height = instance.view.wt.wtDom.outerHeight(instance.$table[0]) + 'px'; - pressed = instance; - } - - var bindManualColumnWidthEvents = function () { - var instance = this; - var dblclick = 0; - var autoresizeTimeout = null; - - this.rootElement.on('mouseenter.handsontable', 'th', function (e) { - if (!pressed) { - refreshResizerPosition.call(instance, e.currentTarget); - } - }); - - this.rootElement.on('mousedown.handsontable', '.manualColumnResizer', function () { - if (autoresizeTimeout == null) { - autoresizeTimeout = setTimeout(function () { - if (dblclick >= 2) { - newSize = instance.determineColumnWidth.call(instance, currentCol); - setManualSize(currentCol, newSize); - instance.forceFullRender = true; - instance.view.render(); //updates all - instance.PluginHooks.run('afterColumnResize', currentCol, newSize); - } - dblclick = 0; - autoresizeTimeout = null; - }, 500); - } - dblclick++; - }); - - this.rootElement.on('mousedown.handsontable', '.manualColumnResizer', function (e) { - startX = e.pageX; - refreshLinePosition.call(instance); - newSize = startWidth; - }); - }; - - this.beforeInit = function () { - this.manualColumnWidths = []; - }; - - this.init = function (source) { - var instance = this; - var manualColumnWidthEnabled = !!(this.getSettings().manualColumnResize); - - if (manualColumnWidthEnabled) { - var initialColumnWidths = this.getSettings().manualColumnResize; - - var loadedManualColumnWidths = loadManualColumnWidths.call(instance); - - if (typeof loadedManualColumnWidths != 'undefined') { - this.manualColumnWidths = loadedManualColumnWidths; - } else if (initialColumnWidths instanceof Array) { - this.manualColumnWidths = initialColumnWidths; - } else { - this.manualColumnWidths = []; - } - - if (source == 'afterInit') { - bindManualColumnWidthEvents.call(this); - instance.forceFullRender = true; - instance.render(); - } - } - }; - - - var setManualSize = function (col, width) { - width = Math.max(width, 20); - - /** - * We need to run col through modifyCol hook, in case the order of displayed columns is different than the order - * in data source. For instance, this order can be modified by manualColumnMove plugin. - */ - col = instance.PluginHooks.execute('modifyCol', col); - - instance.manualColumnWidths[col] = width; - return width; - }; - - this.getColWidth = function (col, response) { - if (this.getSettings().manualColumnResize && this.manualColumnWidths[col]) { - response.width = this.manualColumnWidths[col]; - } - }; -} -var htManualColumnResize = new HandsontableManualColumnResize(); - -Handsontable.PluginHooks.add('beforeInit', htManualColumnResize.beforeInit); -Handsontable.PluginHooks.add('afterInit', function () { - htManualColumnResize.init.call(this, 'afterInit') -}); -Handsontable.PluginHooks.add('afterUpdateSettings', function () { - htManualColumnResize.init.call(this, 'afterUpdateSettings') -}); -Handsontable.PluginHooks.add('afterGetColWidth', htManualColumnResize.getColWidth); - -(function HandsontableObserveChanges() { - - Handsontable.PluginHooks.add('afterLoadData', init); - Handsontable.PluginHooks.add('afterUpdateSettings', init); - - function init() { - var instance = this; - var pluginEnabled = instance.getSettings().observeChanges; - - if (!instance.observer && pluginEnabled) { - createObserver.call(instance); - bindEvents.call(instance); - - } else if (!pluginEnabled){ - destroy.call(instance); - } - } - - function createObserver(){ - var instance = this; - - instance.observeChangesActive = true; - - instance.pauseObservingChanges = function(){ - instance.observeChangesActive = false; - }; - - instance.resumeObservingChanges = function(){ - instance.observeChangesActive = true; - }; - - instance.observer = jsonpatch.observe(instance.getData(), function (patches) { - if(instance.observeChangesActive){ - runHookForOperation.call(instance, patches); - instance.render(); - } - - instance.runHooks('afterChangesObserved'); - }); - } - - function runHookForOperation(rawPatches){ - var instance = this; - var patches = cleanPatches(rawPatches); - - for(var i = 0, len = patches.length; i < len; i++){ - var patch = patches[i]; - var parsedPath = parsePath(patch.path); - - - switch(patch.op){ - case 'add': - if(isNaN(parsedPath.col)){ - instance.runHooks('afterCreateRow', parsedPath.row); - } else { - instance.runHooks('afterCreateCol', parsedPath.col); - } - break; - - case 'remove': - if(isNaN(parsedPath.col)){ - instance.runHooks('afterRemoveRow', parsedPath.row, 1); - } else { - instance.runHooks('afterRemoveCol', parsedPath.col, 1); - } - break; - - case 'replace': - instance.runHooks('afterChange', [parsedPath.row, parsedPath.col, null, patch.value], 'external'); - break; - } - } - - function cleanPatches(rawPatches){ - var patches; - - patches = removeLengthRelatedPatches(rawPatches); - patches = removeMultipleAddOrRemoveColPatches(patches); - - return patches; - } - - /** - * Removing or adding column will produce one patch for each table row. - * This function leaves only one patch for each column add/remove operation - */ - function removeMultipleAddOrRemoveColPatches(rawPatches){ - var newOrRemovedColumns = []; - - return rawPatches.filter(function(patch){ - var parsedPath = parsePath(patch.path); - - if(['add', 'remove'].indexOf(patch.op) != -1 && !isNaN(parsedPath.col)){ - if(newOrRemovedColumns.indexOf(parsedPath.col) != -1){ - return false; - } else { - newOrRemovedColumns.push(parsedPath.col); - } - } - - return true; - }); - - } - - /** - * If observeChanges uses native Object.observe method, then it produces patches for length property. - * This function removes them. - */ - function removeLengthRelatedPatches(rawPatches){ - return rawPatches.filter(function(patch){ - return !/[/]length/ig.test(patch.path); - }) - } - - function parsePath(path){ - var match = path.match(/^\/(\d+)\/?(.*)?$/); - return { - row: parseInt(match[1], 10), - col: /^\d*$/.test(match[2]) ? parseInt(match[2], 10) : match[2] - } - } - } - - function destroy(){ - var instance = this; - - if (instance.observer){ - destroyObserver.call(instance); - unbindEvents.call(instance); - } - } - - function destroyObserver(){ - var instance = this; - - jsonpatch.unobserve(instance.getData(), instance.observer); - delete instance.observeChangesActive; - delete instance.pauseObservingChanges; - delete instance.resumeObservingChanges; - } - - function bindEvents(){ - var instance = this; - instance.addHook('afterDestroy', destroy); - - instance.addHook('afterCreateRow', afterTableAlter); - instance.addHook('afterRemoveRow', afterTableAlter); - - instance.addHook('afterCreateCol', afterTableAlter); - instance.addHook('afterRemoveCol', afterTableAlter); - - instance.addHook('afterChange', function(changes, source){ - if(source != 'loadData'){ - afterTableAlter.call(this); - } - }); - } - - function unbindEvents(){ - var instance = this; - instance.removeHook('afterDestroy', destroy); - - instance.removeHook('afterCreateRow', afterTableAlter); - instance.removeHook('afterRemoveRow', afterTableAlter); - - instance.removeHook('afterCreateCol', afterTableAlter); - instance.removeHook('afterRemoveCol', afterTableAlter); - - instance.removeHook('afterChange', afterTableAlter); - } - - function afterTableAlter(){ - var instance = this; - - instance.pauseObservingChanges(); - - instance.addHookOnce('afterChangesObserved', function(){ - instance.resumeObservingChanges(); - }); - - } -})(); - - -/* - * - * Plugin enables saving table state - * - * */ - - -function Storage(prefix) { - - var savedKeys; - - var saveSavedKeys = function () { - window.localStorage[prefix + '__' + 'persistentStateKeys'] = JSON.stringify(savedKeys); - }; - - var loadSavedKeys = function () { - var keysJSON = window.localStorage[prefix + '__' + 'persistentStateKeys']; - var keys = typeof keysJSON == 'string' ? JSON.parse(keysJSON) : void 0; - savedKeys = keys ? keys : []; - }; - - var clearSavedKeys = function () { - savedKeys = []; - saveSavedKeys(); - }; - - loadSavedKeys(); - - this.saveValue = function (key, value) { - window.localStorage[prefix + '_' + key] = JSON.stringify(value); - if (savedKeys.indexOf(key) == -1) { - savedKeys.push(key); - saveSavedKeys(); - } - - }; - - this.loadValue = function (key, defaultValue) { - - key = typeof key != 'undefined' ? key : defaultValue; - - var value = window.localStorage[prefix + '_' + key]; - - return typeof value == "undefined" ? void 0 : JSON.parse(value); - - }; - - this.reset = function (key) { - window.localStorage.removeItem(prefix + '_' + key); - }; - - this.resetAll = function () { - for (var index = 0; index < savedKeys.length; index++) { - window.localStorage.removeItem(prefix + '_' + savedKeys[index]); - } - - clearSavedKeys(); - }; - -} - - -(function (StorageClass) { - function HandsontablePersistentState() { - var plugin = this; - - - this.init = function () { - var instance = this, - pluginSettings = instance.getSettings()['persistentState']; - - plugin.enabled = !!(pluginSettings); - - if (!plugin.enabled) { - removeHooks.call(instance); - return; - } - - if (!instance.storage) { - instance.storage = new StorageClass(instance.rootElement[0].id); - } - - instance.resetState = plugin.resetValue; - - addHooks.call(instance); - - }; - - this.saveValue = function (key, value) { - var instance = this; - - instance.storage.saveValue(key, value); - }; - - this.loadValue = function (key, saveTo) { - var instance = this; - - saveTo.value = instance.storage.loadValue(key); - }; - - this.resetValue = function (key) { - var instance = this; - - if (typeof key != 'undefined') { - instance.storage.reset(key); - } else { - instance.storage.resetAll(); - } - - }; - - var hooks = { - 'persistentStateSave': plugin.saveValue, - 'persistentStateLoad': plugin.loadValue, - 'persistentStateReset': plugin.resetValue - }; - - function addHooks() { - var instance = this; - - for (var hookName in hooks) { - if (hooks.hasOwnProperty(hookName) && !hookExists.call(instance, hookName)) { - instance.PluginHooks.add(hookName, hooks[hookName]); - } - } - } - - function removeHooks() { - var instance = this; - - for (var hookName in hooks) { - if (hooks.hasOwnProperty(hookName) && hookExists.call(instance, hookName)) { - instance.PluginHooks.remove(hookName, hooks[hookName]); - } - } - } - - function hookExists(hookName) { - var instance = this; - return instance.PluginHooks.hooks['persistent'].hasOwnProperty(hookName); - } - } - - var htPersistentState = new HandsontablePersistentState(); - Handsontable.PluginHooks.add('beforeInit', htPersistentState.init); - Handsontable.PluginHooks.add('afterUpdateSettings', htPersistentState.init); -})(Storage); - -/** - * Handsontable UndoRedo class - */ -(function(Handsontable){ - Handsontable.UndoRedo = function (instance) { - var plugin = this; - this.instance = instance; - this.doneActions = []; - this.undoneActions = []; - this.ignoreNewActions = false; - instance.addHook("afterChange", function (changes, origin) { - if(changes){ - var action = new Handsontable.UndoRedo.ChangeAction(changes); - plugin.done(action); - } - }); - - instance.addHook("afterCreateRow", function (index, amount) { - var action = new Handsontable.UndoRedo.CreateRowAction(index, amount); - plugin.done(action); - }); - - instance.addHook("beforeRemoveRow", function (index, amount) { - var originalData = plugin.instance.getData(); - index = ( originalData.length + index ) % originalData.length; - var removedData = originalData.slice(index, index + amount); - var action = new Handsontable.UndoRedo.RemoveRowAction(index, removedData); - plugin.done(action); - }); - - instance.addHook("afterCreateCol", function (index, amount) { - var action = new Handsontable.UndoRedo.CreateColumnAction(index, amount); - plugin.done(action); - }); - - instance.addHook("beforeRemoveCol", function (index, amount) { - var originalData = plugin.instance.getData(); - index = ( plugin.instance.countCols() + index ) % plugin.instance.countCols(); - var removedData = []; - - for (var i = 0, len = originalData.length; i < len; i++) { - removedData[i] = originalData[i].slice(index, index + amount); - } - - var headers; - if(Handsontable.helper.isArray(instance.getSettings().colHeaders)){ - headers = instance.getSettings().colHeaders.slice(index, index + removedData.length); - } - - var action = new Handsontable.UndoRedo.RemoveColumnAction(index, removedData, headers); - plugin.done(action); - }); - }; - - Handsontable.UndoRedo.prototype.done = function (action) { - if (!this.ignoreNewActions) { - this.doneActions.push(action); - this.undoneActions.length = 0; - } - }; - - /** - * Undo operation from current revision - */ - Handsontable.UndoRedo.prototype.undo = function () { - if (this.isUndoAvailable()) { - var action = this.doneActions.pop(); - - this.ignoreNewActions = true; - action.undo(this.instance); - this.ignoreNewActions = false; - - this.undoneActions.push(action); - } - }; - - /** - * Redo operation from current revision - */ - Handsontable.UndoRedo.prototype.redo = function () { - if (this.isRedoAvailable()) { - var action = this.undoneActions.pop(); - - this.ignoreNewActions = true; - action.redo(this.instance); - this.ignoreNewActions = true; - - this.doneActions.push(action); - } - }; - - /** - * Returns true if undo point is available - * @return {Boolean} - */ - Handsontable.UndoRedo.prototype.isUndoAvailable = function () { - return this.doneActions.length > 0; - }; - - /** - * Returns true if redo point is available - * @return {Boolean} - */ - Handsontable.UndoRedo.prototype.isRedoAvailable = function () { - return this.undoneActions.length > 0; - }; - - /** - * Clears undo history - */ - Handsontable.UndoRedo.prototype.clear = function () { - this.doneActions.length = 0; - this.undoneActions.length = 0; - }; - - Handsontable.UndoRedo.Action = function () { - }; - Handsontable.UndoRedo.Action.prototype.undo = function () { - }; - Handsontable.UndoRedo.Action.prototype.redo = function () { - }; - - Handsontable.UndoRedo.ChangeAction = function (changes) { - this.changes = changes; - }; - Handsontable.helper.inherit(Handsontable.UndoRedo.ChangeAction, Handsontable.UndoRedo.Action); - Handsontable.UndoRedo.ChangeAction.prototype.undo = function (instance) { - var data = $.extend(true, [], this.changes); - for (var i = 0, len = data.length; i < len; i++) { - data[i].splice(3, 1); - } - instance.setDataAtRowProp(data, null, null, 'undo'); - - }; - Handsontable.UndoRedo.ChangeAction.prototype.redo = function (instance) { - var data = $.extend(true, [], this.changes); - for (var i = 0, len = data.length; i < len; i++) { - data[i].splice(2, 1); - } - instance.setDataAtRowProp(data, null, null, 'redo'); - - }; - - Handsontable.UndoRedo.CreateRowAction = function (index, amount) { - this.index = index; - this.amount = amount; - }; - Handsontable.helper.inherit(Handsontable.UndoRedo.CreateRowAction, Handsontable.UndoRedo.Action); - Handsontable.UndoRedo.CreateRowAction.prototype.undo = function (instance) { - instance.alter('remove_row', this.index, this.amount); - }; - Handsontable.UndoRedo.CreateRowAction.prototype.redo = function (instance) { - instance.alter('insert_row', this.index + 1, this.amount); - }; - - Handsontable.UndoRedo.RemoveRowAction = function (index, data) { - this.index = index; - this.data = data; - }; - Handsontable.helper.inherit(Handsontable.UndoRedo.RemoveRowAction, Handsontable.UndoRedo.Action); - Handsontable.UndoRedo.RemoveRowAction.prototype.undo = function (instance) { - var spliceArgs = [this.index, 0]; - Array.prototype.push.apply(spliceArgs, this.data); - - Array.prototype.splice.apply(instance.getData(), spliceArgs); - - instance.render(); - }; - Handsontable.UndoRedo.RemoveRowAction.prototype.redo = function (instance) { - instance.alter('remove_row', this.index, this.data.length); - }; - - Handsontable.UndoRedo.CreateColumnAction = function (index, amount) { - this.index = index; - this.amount = amount; - }; - Handsontable.helper.inherit(Handsontable.UndoRedo.CreateColumnAction, Handsontable.UndoRedo.Action); - Handsontable.UndoRedo.CreateColumnAction.prototype.undo = function (instance) { - instance.alter('remove_col', this.index, this.amount); - }; - Handsontable.UndoRedo.CreateColumnAction.prototype.redo = function (instance) { - instance.alter('insert_col', this.index + 1, this.amount); - }; - - Handsontable.UndoRedo.RemoveColumnAction = function (index, data, headers) { - this.index = index; - this.data = data; - this.amount = this.data[0].length; - this.headers = headers; - }; - Handsontable.helper.inherit(Handsontable.UndoRedo.RemoveColumnAction, Handsontable.UndoRedo.Action); - Handsontable.UndoRedo.RemoveColumnAction.prototype.undo = function (instance) { - var row, spliceArgs; - for (var i = 0, len = instance.getData().length; i < len; i++) { - row = instance.getDataAtRow(i); - - spliceArgs = [this.index, 0]; - Array.prototype.push.apply(spliceArgs, this.data[i]); - - Array.prototype.splice.apply(row, spliceArgs); - - } - - if(typeof this.headers != 'undefined'){ - spliceArgs = [this.index, 0]; - Array.prototype.push.apply(spliceArgs, this.headers) - Array.prototype.splice.apply(instance.getSettings().colHeaders, spliceArgs); - } - - instance.render(); - }; - Handsontable.UndoRedo.RemoveColumnAction.prototype.redo = function (instance) { - instance.alter('remove_col', this.index, this.amount); - }; -})(Handsontable); - -(function(Handsontable){ - - function init(){ - var instance = this; - var pluginEnabled = typeof instance.getSettings().undo == 'undefined' || instance.getSettings().undo; - - if(pluginEnabled){ - if(!instance.undoRedo){ - instance.undoRedo = new Handsontable.UndoRedo(instance); - - exposeUndoRedoMethods(instance); - - instance.addHook('beforeKeyDown', onBeforeKeyDown); - instance.addHook('afterChange', onAfterChange); - } - } else { - if(instance.undoRedo){ - delete instance.undoRedo; - - removeExposedUndoRedoMethods(instance); - - instance.removeHook('beforeKeyDown', onBeforeKeyDown); - instance.removeHook('afterChange', onAfterChange); - } - } - } - - function onBeforeKeyDown(event){ - var instance = this; - - var ctrlDown = (event.ctrlKey || event.metaKey) && !event.altKey; - - if(ctrlDown){ - if (event.keyCode === 89 || (event.shiftKey && event.keyCode === 90)) { //CTRL + Y or CTRL + SHIFT + Z - instance.undoRedo.redo(); - event.stopImmediatePropagation(); - } - else if (event.keyCode === 90) { //CTRL + Z - instance.undoRedo.undo(); - event.stopImmediatePropagation(); - } - } - } - - function onAfterChange(changes, source){ - var instance = this; - if (source == 'loadData'){ - return instance.undoRedo.clear(); - } - } - - function exposeUndoRedoMethods(instance){ - instance.undo = function(){ - return instance.undoRedo.undo(); - }; - - instance.redo = function(){ - return instance.undoRedo.redo(); - }; - - instance.isUndoAvailable = function(){ - return instance.undoRedo.isUndoAvailable(); - }; - - instance.isRedoAvailable = function(){ - return instance.undoRedo.isRedoAvailable(); - }; - - instance.clearUndo = function(){ - return instance.undoRedo.clear(); - }; - } - - function removeExposedUndoRedoMethods(instance){ - delete instance.undo; - delete instance.redo; - delete instance.isUndoAvailable; - delete instance.isRedoAvailable; - delete instance.clearUndo; - } - - Handsontable.PluginHooks.add('afterInit', init); - Handsontable.PluginHooks.add('afterUpdateSettings', init); - -})(Handsontable); -/* - * jQuery.fn.autoResize 1.1+ - * -- - * https://github.com/warpech/jQuery.fn.autoResize - * - * This fork differs from others in a way that it autoresizes textarea in 2-dimensions (horizontally and vertically). - * It was originally forked from alexbardas's repo but maybe should be merged with dpashkevich's repo in future. - * - * originally forked from: - * https://github.com/jamespadolsey/jQuery.fn.autoResize - * which is now located here: - * https://github.com/alexbardas/jQuery.fn.autoResize - * though the mostly maintained for is here: - * https://github.com/dpashkevich/jQuery.fn.autoResize/network - * - * -- - * This program is free software. It comes without any warranty, to - * the extent permitted by applicable law. You can redistribute it - * and/or modify it under the terms of the Do What The Fuck You Want - * To Public License, Version 2, as published by Sam Hocevar. See - * http://sam.zoy.org/wtfpl/COPYING for more details. */ - -(function($){ - - autoResize.defaults = { - onResize: function(){}, - animate: { - duration: 200, - complete: function(){} - }, - extraSpace: 50, - minHeight: 'original', - maxHeight: 500, - minWidth: 'original', - maxWidth: 500 - }; - - autoResize.cloneCSSProperties = [ - 'lineHeight', 'textDecoration', 'letterSpacing', - 'fontSize', 'fontFamily', 'fontStyle', 'fontWeight', - 'textTransform', 'textAlign', 'direction', 'wordSpacing', 'fontSizeAdjust', - 'padding' - ]; - - autoResize.cloneCSSValues = { - position: 'absolute', - top: -9999, - left: -9999, - opacity: 0, - overflow: 'hidden', - overflowX: 'hidden', - overflowY: 'hidden', - border: '1px solid black', - padding: '0.49em' //this must be about the width of caps W character - }; - - autoResize.resizableFilterSelector = 'textarea,input:not(input[type]),input[type=text],input[type=password]'; - - autoResize.AutoResizer = AutoResizer; - - $.fn.autoResize = autoResize; - - function autoResize(config) { - this.filter(autoResize.resizableFilterSelector).each(function(){ - new AutoResizer( $(this), config ); - }); - return this; - } - - function AutoResizer(el, config) { - - if(this.clones) return; - - this.config = $.extend({}, autoResize.defaults, config); - - this.el = el; - - this.nodeName = el[0].nodeName.toLowerCase(); - - this.previousScrollTop = null; - - if (config.maxWidth === 'original') config.maxWidth = el.width(); - if (config.minWidth === 'original') config.minWidth = el.width(); - if (config.maxHeight === 'original') config.maxHeight = el.height(); - if (config.minHeight === 'original') config.minHeight = el.height(); - - if (this.nodeName === 'textarea') { - el.css({ - resize: 'none', - overflowY: 'none' - }); - } - - el.data('AutoResizer', this); - - this.createClone(); - this.injectClone(); - this.bind(); - - } - - AutoResizer.prototype = { - - bind: function() { - - var check = $.proxy(function(){ - this.check(); - return true; - }, this); - - this.unbind(); - - this.el - .bind('keyup.autoResize', check) - //.bind('keydown.autoResize', check) - .bind('change.autoResize', check); - - this.check(null, true); - - }, - - unbind: function() { - this.el.unbind('.autoResize'); - }, - - createClone: function() { - - var el = this.el, - self = this, - config = this.config; - - this.clones = $(); - - if (config.minHeight !== 'original' || config.maxHeight !== 'original') { - this.hClone = el.clone().height('auto'); - this.clones = this.clones.add(this.hClone); - } - if (config.minWidth !== 'original' || config.maxWidth !== 'original') { - this.wClone = $('
    ').width('auto').css({ - whiteSpace: 'nowrap', - 'float': 'left' - }); - this.clones = this.clones.add(this.wClone); - } - - $.each(autoResize.cloneCSSProperties, function(i, p){ - self.clones.css(p, el.css(p)); - }); - - this.clones - .removeAttr('name') - .removeAttr('id') - .attr('tabIndex', -1) - .css(autoResize.cloneCSSValues) - .css('overflowY', 'scroll'); - - }, - - check: function(e, immediate) { - - var config = this.config, - wClone = this.wClone, - hClone = this.hClone, - el = this.el, - value = el.val(); - - if (wClone) { - - wClone.text(value); - - // Calculate new width + whether to change - var cloneWidth = wClone.outerWidth(), - newWidth = (cloneWidth + config.extraSpace) >= config.minWidth ? - cloneWidth + config.extraSpace : config.minWidth, - currentWidth = el.width(); - - newWidth = Math.min(newWidth, config.maxWidth); - - if ( - (newWidth < currentWidth && newWidth >= config.minWidth) || - (newWidth >= config.minWidth && newWidth <= config.maxWidth) - ) { - - config.onResize.call(el); - - el.scrollLeft(0); - - config.animate && !immediate ? - el.stop(1,1).animate({ - width: newWidth - }, config.animate) - : el.width(newWidth); - - } - - } - - if (hClone) { - - if (newWidth) { - hClone.width(newWidth); - } - - hClone.height(0).val(value).scrollTop(10000); - - var scrollTop = hClone[0].scrollTop + config.extraSpace; - - // Don't do anything if scrollTop hasen't changed: - if (this.previousScrollTop === scrollTop) { - return; - } - - this.previousScrollTop = scrollTop; - - if (scrollTop >= config.maxHeight) { - scrollTop = config.maxHeight; - } - - if (scrollTop < config.minHeight) { - scrollTop = config.minHeight; - } - - if(scrollTop == config.maxHeight && newWidth == config.maxWidth) { - el.css('overflowY', 'scroll'); - } - else { - el.css('overflowY', 'hidden'); - } - - config.onResize.call(el); - - // Either animate or directly apply height: - config.animate && !immediate ? - el.stop(1,1).animate({ - height: scrollTop - }, config.animate) - : el.height(scrollTop); - } - }, - - destroy: function() { - this.unbind(); - this.el.removeData('AutoResizer'); - this.clones.remove(); - delete this.el; - delete this.hClone; - delete this.wClone; - delete this.clones; - }, - - injectClone: function() { - ( - autoResize.cloneContainer || - (autoResize.cloneContainer = $('').appendTo('body')) - ).empty().append(this.clones); //this should be refactored so that a node is never cloned more than once - } - - }; - -})(jQuery); -/** - * SheetClip - Spreadsheet Clipboard Parser - * version 0.2 - * - * This tiny library transforms JavaScript arrays to strings that are pasteable by LibreOffice, OpenOffice, - * Google Docs and Microsoft Excel. - * - * Copyright 2012, Marcin Warpechowski - * Licensed under the MIT license. - * http://github.com/warpech/sheetclip/ - */ -/*jslint white: true*/ -(function (global) { - "use strict"; - - function countQuotes(str) { - return str.split('"').length - 1; - } - - global.SheetClip = { - parse: function (str) { - var r, rlen, rows, arr = [], a = 0, c, clen, multiline, last; - rows = str.split('\n'); - if (rows.length > 1 && rows[rows.length - 1] === '') { - rows.pop(); - } - for (r = 0, rlen = rows.length; r < rlen; r += 1) { - rows[r] = rows[r].split('\t'); - for (c = 0, clen = rows[r].length; c < clen; c += 1) { - if (!arr[a]) { - arr[a] = []; - } - if (multiline && c === 0) { - last = arr[a].length - 1; - arr[a][last] = arr[a][last] + '\n' + rows[r][0]; - if (multiline && (countQuotes(rows[r][0]) & 1)) { //& 1 is a bitwise way of performing mod 2 - multiline = false; - arr[a][last] = arr[a][last].substring(0, arr[a][last].length - 1).replace(/""/g, '"'); - } - } - else { - if (c === clen - 1 && rows[r][c].indexOf('"') === 0) { - arr[a].push(rows[r][c].substring(1).replace(/""/g, '"')); - multiline = true; - } - else { - arr[a].push(rows[r][c].replace(/""/g, '"')); - multiline = false; - } - } - } - if (!multiline) { - a += 1; - } - } - return arr; - }, - - stringify: function (arr) { - var r, rlen, c, clen, str = '', val; - for (r = 0, rlen = arr.length; r < rlen; r += 1) { - for (c = 0, clen = arr[r].length; c < clen; c += 1) { - if (c > 0) { - str += '\t'; - } - val = arr[r][c]; - if (typeof val === 'string') { - if (val.indexOf('\n') > -1) { - str += '"' + val.replace(/"/g, '""') + '"'; - } - else { - str += val; - } - } - else if (val === null || val === void 0) { //void 0 resolves to undefined - str += ''; - } - else { - str += val; - } - } - str += '\n'; - } - return str; - } - }; -}(window)); -/** - * CopyPaste.js - * Creates a textarea that stays hidden on the page and gets focused when user presses CTRL while not having a form input focused - * In future we may implement a better driver when better APIs are available - * @constructor - */ -var CopyPaste = (function () { - var instance; - return { - getInstance: function () { - if (!instance) { - instance = new CopyPasteClass(); - } - return instance; - } - }; -})(); - -function CopyPasteClass() { - var that = this - , style - , parent; - - this.copyCallbacks = []; - this.cutCallbacks = []; - this.pasteCallbacks = []; - - var listenerElement = document.documentElement; - parent = document.body; - - if (document.getElementById('CopyPasteDiv')) { - this.elDiv = document.getElementById('CopyPasteDiv'); - this.elTextarea = this.elDiv.firstChild; - } - else { - this.elDiv = document.createElement('DIV'); - this.elDiv.id = 'CopyPasteDiv'; - style = this.elDiv.style; - style.position = 'fixed'; - style.top = 0; - style.left = 0; - parent.appendChild(this.elDiv); - - this.elTextarea = document.createElement('TEXTAREA'); - this.elTextarea.className = 'copyPaste'; - style = this.elTextarea.style; - style.width = '1px'; - style.height = '1px'; - this.elDiv.appendChild(this.elTextarea); - - if (typeof style.opacity !== 'undefined') { - style.opacity = 0; - } - else { - /*@cc_on @if (@_jscript) - if(typeof style.filter === 'string') { - style.filter = 'alpha(opacity=0)'; - } - @end @*/ - } - } - - this._bindEvent(listenerElement, 'keydown', function (event) { - var isCtrlDown = false; - if (event.metaKey) { //mac - isCtrlDown = true; - } - else if (event.ctrlKey && navigator.userAgent.indexOf('Mac') === -1) { //pc - isCtrlDown = true; - } - - if (isCtrlDown) { - if (document.activeElement !== that.elTextarea && (that.getSelectionText() != '' || ['INPUT', 'SELECT', 'TEXTAREA'].indexOf(document.activeElement.nodeName) != -1)) { - return; //this is needed by fragmentSelection in Handsontable. Ignore copypaste.js behavior if fragment of cell text is selected - } - - that.selectNodeText(that.elTextarea); - setTimeout(function () { - that.selectNodeText(that.elTextarea); - }, 0); - } - - /* 67 = c - * 86 = v - * 88 = x - */ - if (isCtrlDown && (event.keyCode === 67 || event.keyCode === 86 || event.keyCode === 88)) { - // that.selectNodeText(that.elTextarea); - - if (event.keyCode === 88) { //works in all browsers, incl. Opera < 12.12 - setTimeout(function () { - that.triggerCut(event); - }, 0); - } - else if (event.keyCode === 86) { - setTimeout(function () { - that.triggerPaste(event); - }, 0); - } - } - }); -} - -//http://jsperf.com/textara-selection -//http://stackoverflow.com/questions/1502385/how-can-i-make-this-code-work-in-ie -CopyPasteClass.prototype.selectNodeText = function (el) { - el.select(); -}; - -//http://stackoverflow.com/questions/5379120/get-the-highlighted-selected-text -CopyPasteClass.prototype.getSelectionText = function () { - var text = ""; - if (window.getSelection) { - text = window.getSelection().toString(); - } else if (document.selection && document.selection.type != "Control") { - text = document.selection.createRange().text; - } - return text; -}; - -CopyPasteClass.prototype.copyable = function (str) { - if (typeof str !== 'string' && str.toString === void 0) { - throw new Error('copyable requires string parameter'); - } - this.elTextarea.value = str; -}; - -/*CopyPasteClass.prototype.onCopy = function (fn) { - this.copyCallbacks.push(fn); -};*/ - -CopyPasteClass.prototype.onCut = function (fn) { - this.cutCallbacks.push(fn); -}; - -CopyPasteClass.prototype.onPaste = function (fn) { - this.pasteCallbacks.push(fn); -}; - -CopyPasteClass.prototype.removeCallback = function (fn) { - var i, ilen; - for (i = 0, ilen = this.copyCallbacks.length; i < ilen; i++) { - if (this.copyCallbacks[i] === fn) { - this.copyCallbacks.splice(i, 1); - return true; - } - } - for (i = 0, ilen = this.cutCallbacks.length; i < ilen; i++) { - if (this.cutCallbacks[i] === fn) { - this.cutCallbacks.splice(i, 1); - return true; - } - } - for (i = 0, ilen = this.pasteCallbacks.length; i < ilen; i++) { - if (this.pasteCallbacks[i] === fn) { - this.pasteCallbacks.splice(i, 1); - return true; - } - } - return false; -}; - -CopyPasteClass.prototype.triggerCut = function (event) { - var that = this; - if (that.cutCallbacks) { - setTimeout(function () { - for (var i = 0, ilen = that.cutCallbacks.length; i < ilen; i++) { - that.cutCallbacks[i](event); - } - }, 50); - } -}; - -CopyPasteClass.prototype.triggerPaste = function (event, str) { - var that = this; - if (that.pasteCallbacks) { - setTimeout(function () { - var val = (str || that.elTextarea.value).replace(/\n$/, ''); //remove trailing newline - for (var i = 0, ilen = that.pasteCallbacks.length; i < ilen; i++) { - that.pasteCallbacks[i](val, event); - } - }, 50); - } -}; - -//old version used this: -// - http://net.tutsplus.com/tutorials/javascript-ajax/javascript-from-null-cross-browser-event-binding/ -// - http://stackoverflow.com/questions/4643249/cross-browser-event-object-normalization -//but that cannot work with jQuery.trigger -CopyPasteClass.prototype._bindEvent = (function () { - if (window.jQuery) { //if jQuery exists, use jQuery event (for compatibility with $.trigger and $.triggerHandler, which can only trigger jQuery events - and we use that in tests) - return function (elem, type, cb) { - $(elem).on(type + '.copypaste', cb); - }; - } - else { - return function (elem, type, cb) { - elem.addEventListener(type, cb, false); //sorry, IE8 will only work with jQuery - }; - } -})(); -// json-patch-duplex.js 0.3.2 -// (c) 2013 Joachim Wester -// MIT license -var jsonpatch; -(function (jsonpatch) { - var objOps = { - add: function (obj, key) { - obj[key] = this.value; - return true; - }, - remove: function (obj, key) { - delete obj[key]; - return true; - }, - replace: function (obj, key) { - obj[key] = this.value; - return true; - }, - move: function (obj, key, tree) { - var temp = { op: "_get", path: this.from }; - apply(tree, [temp]); - apply(tree, [ - { op: "remove", path: this.from } - ]); - apply(tree, [ - { op: "add", path: this.path, value: temp.value } - ]); - return true; - }, - copy: function (obj, key, tree) { - var temp = { op: "_get", path: this.from }; - apply(tree, [temp]); - apply(tree, [ - { op: "add", path: this.path, value: temp.value } - ]); - return true; - }, - test: function (obj, key) { - return (JSON.stringify(obj[key]) === JSON.stringify(this.value)); - }, - _get: function (obj, key) { - this.value = obj[key]; - } - }; - - var arrOps = { - add: function (arr, i) { - arr.splice(i, 0, this.value); - }, - remove: function (arr, i) { - arr.splice(i, 1); - }, - replace: function (arr, i) { - arr[i] = this.value; - }, - move: objOps.move, - copy: objOps.copy, - test: objOps.test, - _get: objOps._get - }; - - var observeOps = { - 'new': function (patches, path) { - var patch = { - op: "add", - path: path + "/" + this.name, - value: this.object[this.name] - }; - patches.push(patch); - }, - deleted: function (patches, path) { - var patch = { - op: "remove", - path: path + "/" + this.name - }; - patches.push(patch); - }, - updated: function (patches, path) { - var patch = { - op: "replace", - path: path + "/" + this.name, - value: this.object[this.name] - }; - patches.push(patch); - } - }; - - // ES6 symbols are not here yet. Used to calculate the json pointer to each object - function markPaths(observer, node) { - for (var key in node) { - if (node.hasOwnProperty(key)) { - var kid = node[key]; - if (kid instanceof Object) { - Object.unobserve(kid, observer); - kid.____Path = node.____Path + "/" + key; - markPaths(observer, kid); - } - } - } - } - - // Detach poor mans ES6 symbols - function clearPaths(observer, node) { - delete node.____Path; - Object.observe(node, observer); - for (var key in node) { - if (node.hasOwnProperty(key)) { - var kid = node[key]; - if (kid instanceof Object) { - clearPaths(observer, kid); - } - } - } - } - - var beforeDict = []; - - //var callbacks = []; this has no purpose - jsonpatch.intervals; - - function unobserve(root, observer) { - if (Object.observe) { - Object.unobserve(root, observer); - markPaths(observer, root); - } else { - clearTimeout(observer.next); - } - } - jsonpatch.unobserve = unobserve; - - function observe(obj, callback) { - var patches = []; - var root = obj; - var observer; - if (Object.observe) { - observer = function (arr) { - if (!root.___Path) { - Object.unobserve(root, observer); - root.____Path = ""; - markPaths(observer, root); - - var a = 0, alen = arr.length; - while (a < alen) { - if (arr[a].name != "____Path") { - observeOps[arr[a].type].call(arr[a], patches, arr[a].object.____Path); - } - a++; - } - - clearPaths(observer, root); - } - if (callback) { - callback(patches); - } - observer.patches = patches; - patches = []; - }; - } else { - observer = {}; - - var mirror; - for (var i = 0, ilen = beforeDict.length; i < ilen; i++) { - if (beforeDict[i].obj === obj) { - mirror = beforeDict[i]; - break; - } - } - - if (!mirror) { - mirror = { obj: obj }; - beforeDict.push(mirror); - } - - mirror.value = JSON.parse(JSON.stringify(obj)); - - if (callback) { - //callbacks.push(callback); this has no purpose - observer.callback = callback; - observer.next = null; - var intervals = this.intervals || [100, 1000, 10000, 60000]; - var currentInterval = 0; - - var dirtyCheck = function () { - generate(observer); - }; - var fastCheck = function () { - clearTimeout(observer.next); - observer.next = setTimeout(function () { - dirtyCheck(); - currentInterval = 0; - observer.next = setTimeout(slowCheck, intervals[currentInterval++]); - }, 0); - }; - var slowCheck = function () { - dirtyCheck(); - if (currentInterval == intervals.length) - currentInterval = intervals.length - 1; - observer.next = setTimeout(slowCheck, intervals[currentInterval++]); - }; - if (typeof window !== 'undefined') { - if (window.addEventListener) { - window.addEventListener('mousedown', fastCheck); - window.addEventListener('mouseup', fastCheck); - window.addEventListener('keydown', fastCheck); - } else { - window.attachEvent('onmousedown', fastCheck); - window.attachEvent('onmouseup', fastCheck); - window.attachEvent('onkeydown', fastCheck); - } - } - observer.next = setTimeout(slowCheck, intervals[currentInterval++]); - } - } - observer.patches = patches; - observer.object = obj; - return _observe(observer, obj, patches); - } - jsonpatch.observe = observe; - - /// Listen to changes on an object tree, accumulate patches - function _observe(observer, obj, patches) { - if (Object.observe) { - Object.observe(obj, observer); - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - var v = obj[key]; - if (v && typeof (v) === "object") { - _observe(observer, v, patches); - } - } - } - } - return observer; - } - - function generate(observer) { - if (Object.observe) { - Object.deliverChangeRecords(observer); - } else { - var mirror; - for (var i = 0, ilen = beforeDict.length; i < ilen; i++) { - if (beforeDict[i].obj === observer.object) { - mirror = beforeDict[i]; - break; - } - } - _generate(mirror.value, observer.object, observer.patches, ""); - } - var temp = observer.patches; - if (temp.length > 0) { - observer.patches = []; - if (observer.callback) { - observer.callback(temp); - } - } - return temp; - } - jsonpatch.generate = generate; - - var _objectKeys; - if (Object.keys) { - _objectKeys = Object.keys; - } else { - _objectKeys = function (obj) { - var keys = []; - for (var o in obj) { - if (obj.hasOwnProperty(o)) { - keys.push(o); - } - } - return keys; - }; - } - - // Dirty check if obj is different from mirror, generate patches and update mirror - function _generate(mirror, obj, patches, path) { - var newKeys = _objectKeys(obj); - var oldKeys = _objectKeys(mirror); - var changed = false; - var deleted = false; - - for (var t = 0; t < oldKeys.length; t++) { - var key = oldKeys[t]; - var oldVal = mirror[key]; - if (obj.hasOwnProperty(key)) { - var newVal = obj[key]; - if (oldVal instanceof Object) { - _generate(oldVal, newVal, patches, path + "/" + key); - } else { - if (oldVal != newVal) { - changed = true; - patches.push({ op: "replace", path: path + "/" + key, value: newVal }); - mirror[key] = newVal; - } - } - } else { - patches.push({ op: "remove", path: path + "/" + key }); - delete mirror[key]; - deleted = true; - } - } - - if (!deleted && newKeys.length == oldKeys.length) { - return; - } - - for (var t = 0; t < newKeys.length; t++) { - var key = newKeys[t]; - if (!mirror.hasOwnProperty(key)) { - patches.push({ op: "add", path: path + "/" + key, value: obj[key] }); - mirror[key] = JSON.parse(JSON.stringify(obj[key])); - } - } - } - - var _isArray; - if (Array.isArray) { - _isArray = Array.isArray; - } else { - _isArray = function (obj) { - return obj.push && typeof obj.length === 'number'; - }; - } - - /// Apply a json-patch operation on an object tree - function apply(tree, patches, listen) { - var result = false, p = 0, plen = patches.length, patch; - while (p < plen) { - patch = patches[p]; - - // Find the object - var keys = patch.path.split('/'); - var obj = tree; - var t = 1; - var len = keys.length; - while (true) { - if (_isArray(obj)) { - var index = parseInt(keys[t], 10); - t++; - if (t >= len) { - result = arrOps[patch.op].call(patch, obj, index, tree); - break; - } - obj = obj[index]; - } else { - var key = keys[t]; - if (key.indexOf('~') != -1) - key = key.replace('~1', '/').replace('~0', '~'); - t++; - if (t >= len) { - result = objOps[patch.op].call(patch, obj, key, tree); - break; - } - obj = obj[key]; - } - } - p++; - } - return result; - } - jsonpatch.apply = apply; -})(jsonpatch || (jsonpatch = {})); - -if (typeof exports !== "undefined") { - exports.apply = jsonpatch.apply; - exports.observe = jsonpatch.observe; - exports.unobserve = jsonpatch.unobserve; - exports.generate = jsonpatch.generate; -} -//# sourceMappingURL=json-patch-duplex.js.map -function WalkontableBorder(instance, settings) { - var style; - - //reference to instance - this.instance = instance; - this.settings = settings; - this.wtDom = this.instance.wtDom; - - this.main = document.createElement("div"); - style = this.main.style; - style.position = 'absolute'; - style.top = 0; - style.left = 0; -// style.visibility = 'hidden'; - - for (var i = 0; i < 5; i++) { - var DIV = document.createElement('DIV'); - DIV.className = 'wtBorder ' + (settings.className || ''); - style = DIV.style; - style.backgroundColor = settings.border.color; - style.height = settings.border.width + 'px'; - style.width = settings.border.width + 'px'; - this.main.appendChild(DIV); - } - - this.top = this.main.childNodes[0]; - this.left = this.main.childNodes[1]; - this.bottom = this.main.childNodes[2]; - this.right = this.main.childNodes[3]; - - - /*$(this.top).on(sss, function(event) { - event.preventDefault(); - event.stopImmediatePropagation(); - $(this).hide(); - }); - $(this.left).on(sss, function(event) { - event.preventDefault(); - event.stopImmediatePropagation(); - $(this).hide(); - }); - $(this.bottom).on(sss, function(event) { - event.preventDefault(); - event.stopImmediatePropagation(); - $(this).hide(); - }); - $(this.right).on(sss, function(event) { - event.preventDefault(); - event.stopImmediatePropagation(); - $(this).hide(); - });*/ - - this.topStyle = this.top.style; - this.leftStyle = this.left.style; - this.bottomStyle = this.bottom.style; - this.rightStyle = this.right.style; - - this.corner = this.main.childNodes[4]; - this.corner.className += ' corner'; - this.cornerStyle = this.corner.style; - this.cornerStyle.width = '5px'; - this.cornerStyle.height = '5px'; - this.cornerStyle.border = '2px solid #FFF'; - - this.disappear(); - if (!instance.wtTable.bordersHolder) { - instance.wtTable.bordersHolder = document.createElement('div'); - instance.wtTable.bordersHolder.className = 'htBorders'; - instance.wtTable.hider.appendChild(instance.wtTable.bordersHolder); - - } - instance.wtTable.bordersHolder.appendChild(this.main); - - var down = false; - var $body = $(document.body); - - $body.on('mousedown.walkontable.' + instance.guid, function () { - down = true; - }); - - $body.on('mouseup.walkontable.' + instance.guid, function () { - down = false - }); - - $(this.main.childNodes).on('mouseenter', function (event) { - if (!down || !instance.getSetting('hideBorderOnMouseDownOver')) { - return; - } - event.preventDefault(); - event.stopImmediatePropagation(); - - var bounds = this.getBoundingClientRect(); - - var $this = $(this); - $this.hide(); - - var isOutside = function (event) { - if (event.clientY < Math.floor(bounds.top)) { - return true; - } - if (event.clientY > Math.ceil(bounds.top + bounds.height)) { - return true; - } - if (event.clientX < Math.floor(bounds.left)) { - return true; - } - if (event.clientX > Math.ceil(bounds.left + bounds.width)) { - return true; - } - }; - - $body.on('mousemove.border.' + instance.guid, function (event) { - if (isOutside(event)) { - $body.off('mousemove.border.' + instance.guid); - $this.show(); - } - }); - }); -} - -/** - * Show border around one or many cells - * @param {Array} corners - */ -WalkontableBorder.prototype.appear = function (corners) { - var isMultiple, fromTD, toTD, fromOffset, toOffset, containerOffset, top, minTop, left, minLeft, height, width; - if (this.disabled) { - return; - } - - var instance = this.instance - , fromRow - , fromColumn - , toRow - , toColumn - , hideTop = false - , hideLeft = false - , hideBottom = false - , hideRight = false - , i - , ilen - , s; - - if (!instance.wtTable.isRowInViewport(corners[0])) { - hideTop = true; - } - - if (!instance.wtTable.isRowInViewport(corners[2])) { - hideBottom = true; - } - - ilen = instance.wtTable.rowStrategy.countVisible(); - - for (i = 0; i < ilen; i++) { - s = instance.wtTable.rowFilter.visibleToSource(i); - if (s >= corners[0] && s <= corners[2]) { - fromRow = s; - break; - } - } - - for (i = ilen - 1; i >= 0; i--) { - s = instance.wtTable.rowFilter.visibleToSource(i); - if (s >= corners[0] && s <= corners[2]) { - toRow = s; - break; - } - } - - if (hideTop && hideBottom) { - hideLeft = true; - hideRight = true; - } - else { - if (!instance.wtTable.isColumnInViewport(corners[1])) { - hideLeft = true; - } - - if (!instance.wtTable.isColumnInViewport(corners[3])) { - hideRight = true; - } - - ilen = instance.wtTable.columnStrategy.countVisible(); - - for (i = 0; i < ilen; i++) { - s = instance.wtTable.columnFilter.visibleToSource(i); - if (s >= corners[1] && s <= corners[3]) { - fromColumn = s; - break; - } - } - - for (i = ilen - 1; i >= 0; i--) { - s = instance.wtTable.columnFilter.visibleToSource(i); - if (s >= corners[1] && s <= corners[3]) { - toColumn = s; - break; - } - } - } - - if (fromRow !== void 0 && fromColumn !== void 0) { - isMultiple = (fromRow !== toRow || fromColumn !== toColumn); - fromTD = instance.wtTable.getCell([fromRow, fromColumn]); - toTD = isMultiple ? instance.wtTable.getCell([toRow, toColumn]) : fromTD; - fromOffset = this.wtDom.offset(fromTD); - toOffset = isMultiple ? this.wtDom.offset(toTD) : fromOffset; - containerOffset = this.wtDom.offset(instance.wtTable.TABLE); - - minTop = fromOffset.top; - height = toOffset.top + this.wtDom.outerHeight(toTD) - minTop; - minLeft = fromOffset.left; - width = toOffset.left + this.wtDom.outerWidth(toTD) - minLeft; - - top = minTop - containerOffset.top - 1; - left = minLeft - containerOffset.left - 1; - - var style = this.wtDom.getComputedStyle(fromTD); - if (parseInt(style['borderTopWidth'], 10) > 0) { - top += 1; - height -= 1; - } - if (parseInt(style['borderLeftWidth'], 10) > 0) { - left += 1; - width -= 1; - } - } - else { - this.disappear(); - return; - } - - if (hideTop) { - this.topStyle.display = 'none'; - } - else { - this.topStyle.top = top + 'px'; - this.topStyle.left = left + 'px'; - this.topStyle.width = width + 'px'; - this.topStyle.display = 'block'; - } - - if (hideLeft) { - this.leftStyle.display = 'none'; - } - else { - this.leftStyle.top = top + 'px'; - this.leftStyle.left = left + 'px'; - this.leftStyle.height = height + 'px'; - this.leftStyle.display = 'block'; - } - - var delta = Math.floor(this.settings.border.width / 2); - - if (hideBottom) { - this.bottomStyle.display = 'none'; - } - else { - this.bottomStyle.top = top + height - delta + 'px'; - this.bottomStyle.left = left + 'px'; - this.bottomStyle.width = width + 'px'; - this.bottomStyle.display = 'block'; - } - - if (hideRight) { - this.rightStyle.display = 'none'; - } - else { - this.rightStyle.top = top + 'px'; - this.rightStyle.left = left + width - delta + 'px'; - this.rightStyle.height = height + 1 + 'px'; - this.rightStyle.display = 'block'; - } - - if (hideBottom || hideRight || !this.hasSetting(this.settings.border.cornerVisible)) { - this.cornerStyle.display = 'none'; - } - else { - this.cornerStyle.top = top + height - 4 + 'px'; - this.cornerStyle.left = left + width - 4 + 'px'; - this.cornerStyle.display = 'block'; - } -}; - -/** - * Hide border - */ -WalkontableBorder.prototype.disappear = function () { - this.topStyle.display = 'none'; - this.leftStyle.display = 'none'; - this.bottomStyle.display = 'none'; - this.rightStyle.display = 'none'; - this.cornerStyle.display = 'none'; -}; - -WalkontableBorder.prototype.hasSetting = function (setting) { - if (typeof setting === 'function') { - return setting(); - } - return !!setting; -}; -/** - * WalkontableCellFilter - * @constructor - */ -function WalkontableCellFilter() { - this.offset = 0; - this.total = 0; - this.fixedCount = 0; -} - -WalkontableCellFilter.prototype.source = function (n) { - return n; -}; - -WalkontableCellFilter.prototype.offsetted = function (n) { - return n + this.offset; -}; - -WalkontableCellFilter.prototype.unOffsetted = function (n) { - return n - this.offset; -}; - -WalkontableCellFilter.prototype.fixed = function (n) { - if (n < this.fixedCount) { - return n - this.offset; - } - else { - return n; - } -}; - -WalkontableCellFilter.prototype.unFixed = function (n) { - if (n < this.fixedCount) { - return n + this.offset; - } - else { - return n; - } -}; - -WalkontableCellFilter.prototype.visibleToSource = function (n) { - return this.source(this.offsetted(this.fixed(n))); -}; - -WalkontableCellFilter.prototype.sourceToVisible = function (n) { - return this.source(this.unOffsetted(this.unFixed(n))); -}; -/** - * WalkontableCellStrategy - * @constructor - */ -function WalkontableCellStrategy() { -} - -WalkontableCellStrategy.prototype.getSize = function (index) { - return this.cellSizes[index]; -}; - -WalkontableCellStrategy.prototype.getContainerSize = function (proposedSize) { - return typeof this.containerSizeFn === 'function' ? this.containerSizeFn(proposedSize) : this.containerSizeFn; -}; - -WalkontableCellStrategy.prototype.countVisible = function () { - return this.cellCount; -}; - -WalkontableCellStrategy.prototype.isLastIncomplete = function () { - return this.remainingSize >= 0; -}; -/** - * WalkontableClassNameList - * @constructor - */ -function WalkontableClassNameCache() { - this.cache = []; -} - -WalkontableClassNameCache.prototype.add = function (r, c, cls) { - if (!this.cache[r]) { - this.cache[r] = []; - } - if (!this.cache[r][c]) { - this.cache[r][c] = []; - } - this.cache[r][c][cls] = true; -}; - -WalkontableClassNameCache.prototype.test = function (r, c, cls) { - return (this.cache[r] && this.cache[r][c] && this.cache[r][c][cls]); -}; -/** - * WalkontableColumnFilter - * @constructor - */ -function WalkontableColumnFilter() { - this.countTH = 0; -} - -WalkontableColumnFilter.prototype = new WalkontableCellFilter(); - -WalkontableColumnFilter.prototype.readSettings = function (instance) { - this.offset = instance.wtSettings.settings.offsetColumn; - this.total = instance.getSetting('totalColumns'); - this.fixedCount = instance.getSetting('fixedColumnsLeft'); - this.countTH = instance.getSetting('rowHeaders').length; -}; - -WalkontableColumnFilter.prototype.offsettedTH = function (n) { - return n - this.countTH; -}; - -WalkontableColumnFilter.prototype.unOffsettedTH = function (n) { - return n + this.countTH; -}; - -WalkontableColumnFilter.prototype.visibleRowHeadedColumnToSourceColumn = function (n) { - return this.visibleToSource(this.offsettedTH(n)); -}; - -WalkontableColumnFilter.prototype.sourceColumnToVisibleRowHeadedColumn = function (n) { - return this.unOffsettedTH(this.sourceToVisible(n)); -}; -/** - * WalkontableColumnStrategy - * @param containerSizeFn - * @param sizeAtIndex - * @param strategy - all, last, none - * @constructor - */ -function WalkontableColumnStrategy(containerSizeFn, sizeAtIndex, strategy) { - var size - , i = 0; - - this.containerSizeFn = containerSizeFn; - this.cellSizesSum = 0; - this.cellSizes = []; - this.cellStretch = []; - this.cellCount = 0; - this.remainingSize = 0; - this.strategy = strategy; - - //step 1 - determine cells that fit containerSize and cache their widths - while (true) { - size = sizeAtIndex(i); - if (size === void 0) { - break; //total columns exceeded - } - if (this.cellSizesSum >= this.getContainerSize(this.cellSizesSum + size)) { - break; //total width exceeded - } - this.cellSizes.push(size); - this.cellSizesSum += size; - this.cellCount++; - - i++; - } - - var containerSize = this.getContainerSize(this.cellSizesSum); - this.remainingSize = this.cellSizesSum - containerSize; - //negative value means the last cell is fully visible and there is some space left for stretching - //positive value means the last cell is not fully visible -} - -WalkontableColumnStrategy.prototype = new WalkontableCellStrategy(); - -WalkontableColumnStrategy.prototype.getSize = function (index) { - return this.cellSizes[index] + (this.cellStretch[index] || 0); -}; - -WalkontableColumnStrategy.prototype.stretch = function () { - //step 2 - apply stretching strategy - var containerSize = this.getContainerSize(this.cellSizesSum) - , i = 0; - this.remainingSize = this.cellSizesSum - containerSize; - - this.cellStretch.length = 0; //clear previous stretch - - if (this.strategy === 'all') { - if (this.remainingSize < 0) { - var ratio = containerSize / this.cellSizesSum; - var newSize; - - while (i < this.cellCount - 1) { //"i < this.cellCount - 1" is needed because last cellSize is adjusted after the loop - newSize = Math.floor(ratio * this.cellSizes[i]); - this.remainingSize += newSize - this.cellSizes[i]; - this.cellStretch[i] = newSize - this.cellSizes[i]; - i++; - } - this.cellStretch[this.cellCount - 1] = -this.remainingSize; - this.remainingSize = 0; - } - } - else if (this.strategy === 'last') { - if (this.remainingSize < 0) { - this.cellStretch[this.cellCount - 1] = -this.remainingSize; - this.remainingSize = 0; - } - } -}; -function Walkontable(settings) { - var that = this, - originalHeaders = []; - - this.guid = 'wt_' + (window.Handsontable ? Handsontable.helper.randomString() : ''); //this is the namespace for global events - - //bootstrap from settings - this.wtSettings = new WalkontableSettings(this, settings); - this.wtDom = new WalkontableDom(); - this.wtTable = new WalkontableTable(this); - this.wtScroll = new WalkontableScroll(this); - this.wtViewport = new WalkontableViewport(this); - this.wtScrollbars = new WalkontableScrollbars(this); - this.wtWheel = new WalkontableWheel(this); - this.wtEvent = new WalkontableEvent(this); - - //find original headers - if (this.wtTable.THEAD.childNodes.length && this.wtTable.THEAD.childNodes[0].childNodes.length) { - for (var c = 0, clen = this.wtTable.THEAD.childNodes[0].childNodes.length; c < clen; c++) { - originalHeaders.push(this.wtTable.THEAD.childNodes[0].childNodes[c].innerHTML); - } - if (!this.getSetting('columnHeaders').length) { - this.update('columnHeaders', [function (column, TH) { - that.wtDom.fastInnerText(TH, originalHeaders[column]); - }]); - } - } - - //initialize selections - this.selections = {}; - var selectionsSettings = this.getSetting('selections'); - if (selectionsSettings) { - for (var i in selectionsSettings) { - if (selectionsSettings.hasOwnProperty(i)) { - this.selections[i] = new WalkontableSelection(this, selectionsSettings[i]); - } - } - } - - this.drawn = false; - this.drawInterrupted = false; - - if (window.Handsontable) { - Handsontable.PluginHooks.add('beforeChange', function () { - if (that.rowHeightCache) { - that.rowHeightCache.length = 0; - } - }); - - } -} - -Walkontable.prototype.draw = function (selectionsOnly) { - this.drawInterrupted = false; - if (!selectionsOnly && !this.wtDom.isVisible(this.wtTable.TABLE)) { - this.drawInterrupted = true; //draw interrupted because TABLE is not visible - return; - } - - this.getSetting('beforeDraw', !selectionsOnly); - selectionsOnly = selectionsOnly && this.getSetting('offsetRow') === this.lastOffsetRow && this.getSetting('offsetColumn') === this.lastOffsetColumn; - if (this.drawn) { //fix offsets that might have changed - this.scrollVertical(0); - this.scrollHorizontal(0); - } - this.lastOffsetRow = this.getSetting('offsetRow'); - this.lastOffsetColumn = this.getSetting('offsetColumn'); - this.wtTable.draw(selectionsOnly); - this.getSetting('onDraw', !selectionsOnly); - return this; -}; - -Walkontable.prototype.update = function (settings, value) { - return this.wtSettings.update(settings, value); -}; - -Walkontable.prototype.scrollVertical = function (delta) { - return this.wtScroll.scrollVertical(delta); -}; - -Walkontable.prototype.scrollHorizontal = function (delta) { - return this.wtScroll.scrollHorizontal(delta); -}; - -Walkontable.prototype.scrollViewport = function (coords) { - this.wtScroll.scrollViewport(coords); - return this; -}; - -Walkontable.prototype.getViewport = function () { - return [ - this.wtTable.rowFilter.visibleToSource(0), - this.wtTable.columnFilter.visibleToSource(0), - this.wtTable.getLastVisibleRow(), - this.wtTable.getLastVisibleColumn() - ]; -}; - -Walkontable.prototype.getSetting = function (key, param1, param2, param3) { - return this.wtSettings.getSetting(key, param1, param2, param3); -}; - -Walkontable.prototype.hasSetting = function (key) { - return this.wtSettings.has(key); -}; - -Walkontable.prototype.destroy = function () { - $(document.body).off('.' + this.guid); - this.wtScrollbars.destroy(); - clearTimeout(this.wheelTimeout); - clearTimeout(this.dblClickTimeout); -}; -function WalkontableDom() { -} - -//goes up the DOM tree (including given element) until it finds an element that matches the nodeName -WalkontableDom.prototype.closest = function (elem, nodeNames, until) { - while (elem != null && elem !== until) { - if (elem.nodeType === 1 && nodeNames.indexOf(elem.nodeName) > -1) { - return elem; - } - elem = elem.parentNode; - } - return null; -}; - -//goes up the DOM tree and checks if element is child of another element -WalkontableDom.prototype.isChildOf = function (child, parent) { - var node = child.parentNode; - while (node != null) { - if (node == parent) { - return true; - } - node = node.parentNode; - } - return false; -}; - -/** - * Counts index of element within its parent - * WARNING: for performance reasons, assumes there are only element nodes (no text nodes). This is true for Walkotnable - * Otherwise would need to check for nodeType or use previousElementSibling - * @see http://jsperf.com/sibling-index/10 - * @param {Element} elem - * @return {Number} - */ -WalkontableDom.prototype.index = function (elem) { - var i = 0; - while (elem = elem.previousSibling) { - ++i - } - return i; -}; - -if (document.documentElement.classList) { - // HTML5 classList API - WalkontableDom.prototype.hasClass = function (ele, cls) { - return ele.classList.contains(cls); - }; - - WalkontableDom.prototype.addClass = function (ele, cls) { - ele.classList.add(cls); - }; - - WalkontableDom.prototype.removeClass = function (ele, cls) { - ele.classList.remove(cls); - }; -} -else { - //http://snipplr.com/view/3561/addclass-removeclass-hasclass/ - WalkontableDom.prototype.hasClass = function (ele, cls) { - return ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)')); - }; - - WalkontableDom.prototype.addClass = function (ele, cls) { - if (!this.hasClass(ele, cls)) ele.className += " " + cls; - }; - - WalkontableDom.prototype.removeClass = function (ele, cls) { - if (this.hasClass(ele, cls)) { //is this really needed? - var reg = new RegExp('(\\s|^)' + cls + '(\\s|$)'); - ele.className = ele.className.replace(reg, ' ').replace(/^\s\s*/, '').replace(/\s\s*$/, ''); //last 2 replaces do right trim (see http://blog.stevenlevithan.com/archives/faster-trim-javascript) - } - }; -} - -/*//http://net.tutsplus.com/tutorials/javascript-ajax/javascript-from-null-cross-browser-event-binding/ - WalkontableDom.prototype.addEvent = (function () { - var that = this; - if (document.addEventListener) { - return function (elem, type, cb) { - if ((elem && !elem.length) || elem === window) { - elem.addEventListener(type, cb, false); - } - else if (elem && elem.length) { - var len = elem.length; - for (var i = 0; i < len; i++) { - that.addEvent(elem[i], type, cb); - } - } - }; - } - else { - return function (elem, type, cb) { - if ((elem && !elem.length) || elem === window) { - elem.attachEvent('on' + type, function () { - - //normalize - //http://stackoverflow.com/questions/4643249/cross-browser-event-object-normalization - var e = window['event']; - e.target = e.srcElement; - //e.offsetX = e.layerX; - //e.offsetY = e.layerY; - e.relatedTarget = e.relatedTarget || e.type == 'mouseover' ? e.fromElement : e.toElement; - if (e.target.nodeType === 3) e.target = e.target.parentNode; //Safari bug - - return cb.call(elem, e) - }); - } - else if (elem.length) { - var len = elem.length; - for (var i = 0; i < len; i++) { - that.addEvent(elem[i], type, cb); - } - } - }; - } - })(); - - WalkontableDom.prototype.triggerEvent = function (element, eventName, target) { - var event; - if (document.createEvent) { - event = document.createEvent("MouseEvents"); - event.initEvent(eventName, true, true); - } else { - event = document.createEventObject(); - event.eventType = eventName; - } - - event.eventName = eventName; - event.target = target; - - if (document.createEvent) { - target.dispatchEvent(event); - } else { - target.fireEvent("on" + event.eventType, event); - } - };*/ - -WalkontableDom.prototype.removeTextNodes = function (elem, parent) { - if (elem.nodeType === 3) { - parent.removeChild(elem); //bye text nodes! - } - else if (['TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR'].indexOf(elem.nodeName) > -1) { - var childs = elem.childNodes; - for (var i = childs.length - 1; i >= 0; i--) { - this.removeTextNodes(childs[i], elem); - } - } -}; - -/** - * Remove childs function - * WARNING - this doesn't unload events and data attached by jQuery - * http://jsperf.com/jquery-html-vs-empty-vs-innerhtml/9 - * http://jsperf.com/jquery-html-vs-empty-vs-innerhtml/11 - no siginificant improvement with Chrome remove() method - * @param element - * @returns {void} - */ -// -WalkontableDom.prototype.empty = function (element) { - var child; - while (child = element.lastChild) { - element.removeChild(child); - } -}; - -WalkontableDom.prototype.HTML_CHARACTERS = /(<(.*)>|&(.*);)/; - -/** - * Insert content into element trying avoid innerHTML method. - * @return {void} - */ -WalkontableDom.prototype.fastInnerHTML = function (element, content) { - if (this.HTML_CHARACTERS.test(content)) { - element.innerHTML = content; - } - else { - this.fastInnerText(element, content); - } -}; - -/** - * Insert text content into element - * @return {void} - */ -if (document.createTextNode('test').textContent) { //STANDARDS - WalkontableDom.prototype.fastInnerText = function (element, content) { - var child = element.firstChild; - if (child && child.nodeType === 3 && child.nextSibling === null) { - //fast lane - replace existing text node - //http://jsperf.com/replace-text-vs-reuse - child.textContent = content; - } - else { - //slow lane - empty element and insert a text node - this.empty(element); - element.appendChild(document.createTextNode(content)); - } - }; -} -else { //IE8 - WalkontableDom.prototype.fastInnerText = function (element, content) { - var child = element.firstChild; - if (child && child.nodeType === 3 && child.nextSibling === null) { - //fast lane - replace existing text node - //http://jsperf.com/replace-text-vs-reuse - child.data = content; - } - else { - //slow lane - empty element and insert a text node - this.empty(element); - element.appendChild(document.createTextNode(content)); - } - }; -} - -/** - * Returns true if element is attached to the DOM and visible, false otherwise - * @param elem - * @returns {boolean} - */ -WalkontableDom.prototype.isVisible = function (elem) { - //fast method - try {//try/catch performance is not a problem here: http://jsperf.com/try-catch-performance-overhead/7 - if (!elem.offsetParent) { - return false; //fixes problem with UI Bootstrap directive - } - } - catch (e) { - return false; //IE8 throws "Unspecified error" when offsetParent is not found - we catch it here - } - -// if (elem.offsetWidth > 0 || (elem.parentNode && elem.parentNode.offsetWidth > 0)) { //IE10 was mistaken here - if (elem.offsetWidth > 0) { - return true; - } - - //slow method - var next = elem; - while (next !== document.documentElement) { //until reached - if (next === null) { //parent detached from DOM - return false; - } - else if (next.nodeType === 11) { - return true; - } - else if (next.style.display === 'none') { - return false; - } - next = next.parentNode; - } - return true; -}; - -/** - * Returns elements top and left offset relative to the document. In our usage case compatible with jQuery but 2x faster - * @param {HTMLElement} elem - * @return {Object} - */ -WalkontableDom.prototype.offset = function (elem) { - if (this.hasCaptionProblem() && elem.firstChild && elem.firstChild.nodeName === 'CAPTION') { - //fixes problem with Firefox ignoring
    '; - - var CAPTION = document.createElement('CAPTION'); - CAPTION.innerHTML = 'c
    c
    c
    c'; - CAPTION.style.padding = 0; - CAPTION.style.margin = 0; - TABLE.insertBefore(CAPTION, TBODY); - - document.body.appendChild(TABLE); - hasCaptionProblem = (TABLE.offsetHeight < 2 * TABLE.lastChild.offsetHeight); //boolean - document.body.removeChild(TABLE); - } - - WalkontableDom.prototype.hasCaptionProblem = function () { - if (hasCaptionProblem === void 0) { - detectCaptionProblem(); - } - return hasCaptionProblem; - }; - - /** - * Returns caret position in text input - * @author http://stackoverflow.com/questions/263743/how-to-get-caret-position-in-textarea - * @return {Number} - */ - WalkontableDom.prototype.getCaretPosition = function (el) { - if (el.selectionStart) { - return el.selectionStart; - } - else if (document.selection) { //IE8 - el.focus(); - var r = document.selection.createRange(); - if (r == null) { - return 0; - } - var re = el.createTextRange(), - rc = re.duplicate(); - re.moveToBookmark(r.getBookmark()); - rc.setEndPoint('EndToStart', re); - return rc.text.length; - } - return 0; - }; - - /** - * Sets caret position in text input - * @author http://blog.vishalon.net/index.php/javascript-getting-and-setting-caret-position-in-textarea/ - * @param {Element} el - * @param {Number} pos - */ - WalkontableDom.prototype.setCaretPosition = function (el, pos) { - if (el.setSelectionRange) { - el.focus(); - el.setSelectionRange(pos, pos); - } - else if (el.createTextRange) { //IE8 - var range = el.createTextRange(); - range.collapse(true); - range.moveEnd('character', pos); - range.moveStart('character', pos); - range.select(); - } - }; -})(); - -function WalkontableEvent(instance) { - var that = this; - - //reference to instance - this.instance = instance; - - this.wtDom = this.instance.wtDom; - - var dblClickOrigin = [null, null, null, null]; - this.instance.dblClickTimeout = null; - - var onMouseDown = function (event) { - var cell = that.parentCell(event.target); - - if (cell.TD && cell.TD.nodeName === 'TD') { - if (that.instance.hasSetting('onCellMouseDown')) { - that.instance.getSetting('onCellMouseDown', event, cell.coords, cell.TD); - } - } - else if (that.wtDom.hasClass(event.target, 'corner')) { - that.instance.getSetting('onCellCornerMouseDown', event, event.target); - } - - if (event.button !== 2) { //if not right mouse button - if (cell.TD && cell.TD.nodeName === 'TD') { - dblClickOrigin.shift(); - dblClickOrigin.push(cell.TD); - } - else if (that.wtDom.hasClass(event.target, 'corner')) { - dblClickOrigin.shift(); - dblClickOrigin.push(event.target); - } - } - }; - - var lastMouseOver; - var onMouseOver = function (event) { - if (that.instance.hasSetting('onCellMouseOver')) { - var TABLE = that.instance.wtTable.TABLE; - var TD = that.wtDom.closest(event.target, ['TD', 'TH'], TABLE); - if (TD && TD !== lastMouseOver && that.wtDom.isChildOf(TD, TABLE)) { - lastMouseOver = TD; - if (TD.nodeName === 'TD') { - that.instance.getSetting('onCellMouseOver', event, that.instance.wtTable.getCoords(TD), TD); - } - } - } - }; - -/* var lastMouseOut; - var onMouseOut = function (event) { - if (that.instance.hasSetting('onCellMouseOut')) { - var TABLE = that.instance.wtTable.TABLE; - var TD = that.wtDom.closest(event.target, ['TD', 'TH'], TABLE); - if (TD && TD !== lastMouseOut && that.wtDom.isChildOf(TD, TABLE)) { - lastMouseOut = TD; - if (TD.nodeName === 'TD') { - that.instance.getSetting('onCellMouseOut', event, that.instance.wtTable.getCoords(TD), TD); - } - } - } - };*/ - - var onMouseUp = function (event) { - if (event.button !== 2) { //if not right mouse button - var cell = that.parentCell(event.target); - - if (cell.TD && cell.TD.nodeName === 'TD') { - dblClickOrigin.shift(); - dblClickOrigin.push(cell.TD); - } - else { - dblClickOrigin.shift(); - dblClickOrigin.push(event.target); - } - - if (dblClickOrigin[3] !== null && dblClickOrigin[3] === dblClickOrigin[2]) { - if (that.instance.dblClickTimeout && dblClickOrigin[2] === dblClickOrigin[1] && dblClickOrigin[1] === dblClickOrigin[0]) { - if (cell.TD) { - that.instance.getSetting('onCellDblClick', event, cell.coords, cell.TD); - } - else if (that.wtDom.hasClass(event.target, 'corner')) { - that.instance.getSetting('onCellCornerDblClick', event, cell.coords, cell.TD); - } - - clearTimeout(that.instance.dblClickTimeout); - that.instance.dblClickTimeout = null; - } - else { - clearTimeout(that.instance.dblClickTimeout); - that.instance.dblClickTimeout = setTimeout(function () { - that.instance.dblClickTimeout = null; - }, 500); - } - } - } - }; - - $(this.instance.wtTable.holder).on('mousedown', onMouseDown); - $(this.instance.wtTable.TABLE).on('mouseover', onMouseOver); -// $(this.instance.wtTable.TABLE).on('mouseout', onMouseOut); - $(this.instance.wtTable.holder).on('mouseup', onMouseUp); -} - -WalkontableEvent.prototype.parentCell = function (elem) { - var cell = {}; - var TABLE = this.instance.wtTable.TABLE; - var TD = this.wtDom.closest(elem, ['TD', 'TH'], TABLE); - if (TD && this.wtDom.isChildOf(TD, TABLE)) { - cell.coords = this.instance.wtTable.getCoords(TD); - cell.TD = TD; - } - else if (this.wtDom.hasClass(elem, 'wtBorder') && this.wtDom.hasClass(elem, 'current') && !this.wtDom.hasClass(elem, 'corner')) { - cell.coords = this.instance.selections.current.selected[0]; - cell.TD = this.instance.wtTable.getCell(cell.coords); - } - return cell; -}; -function walkontableRangesIntersect() { - var from = arguments[0]; - var to = arguments[1]; - for (var i = 1, ilen = arguments.length / 2; i < ilen; i++) { - if (from <= arguments[2 * i + 1] && to >= arguments[2 * i]) { - return true; - } - } - return false; -} -//http://stackoverflow.com/questions/3629183/why-doesnt-indexof-work-on-an-array-ie8 -if (!Array.prototype.indexOf) { - Array.prototype.indexOf = function (elt /*, from*/) { - var len = this.length >>> 0; - - var from = Number(arguments[1]) || 0; - from = (from < 0) - ? Math.ceil(from) - : Math.floor(from); - if (from < 0) - from += len; - - for (; from < len; from++) { - if (from in this && - this[from] === elt) - return from; - } - return -1; - }; -} - -/** - * http://notes.jetienne.com/2011/05/18/cancelRequestAnimFrame-for-paul-irish-requestAnimFrame.html - */ -window.requestAnimFrame = (function () { - return window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame || - function (/* function */ callback, /* DOMElement */ element) { - return window.setTimeout(callback, 1000 / 60); - }; -})(); - -window.cancelRequestAnimFrame = (function () { - return window.cancelAnimationFrame || - window.webkitCancelRequestAnimationFrame || - window.mozCancelRequestAnimationFrame || - window.oCancelRequestAnimationFrame || - window.msCancelRequestAnimationFrame || - clearTimeout -})(); - -//http://snipplr.com/view/13523/ -//modified for speed -//http://jsperf.com/getcomputedstyle-vs-style-vs-css/8 -if (!window.getComputedStyle) { - (function () { - var elem; - - var styleObj = { - getPropertyValue: function getPropertyValue(prop) { - if (prop == 'float') prop = 'styleFloat'; - return elem.currentStyle[prop.toUpperCase()] || null; - } - } - - window.getComputedStyle = function (el) { - elem = el; - return styleObj; - } - })(); -} -/** - * WalkontableRowFilter - * @constructor - */ -function WalkontableRowFilter() { -} - -WalkontableRowFilter.prototype = new WalkontableCellFilter(); - -WalkontableRowFilter.prototype.readSettings = function (instance) { - this.offset = instance.wtSettings.settings.offsetRow; - this.total = instance.getSetting('totalRows'); - this.fixedCount = instance.getSetting('fixedRowsTop'); -}; -/** - * WalkontableRowStrategy - * @param containerSizeFn - * @param sizeAtIndex - * @constructor - */ -function WalkontableRowStrategy(containerSizeFn, sizeAtIndex) { - this.containerSizeFn = containerSizeFn; - this.sizeAtIndex = sizeAtIndex; - this.cellSizesSum = 0; - this.cellSizes = []; - this.cellCount = 0; - this.remainingSize = -Infinity; -} - -WalkontableRowStrategy.prototype = new WalkontableCellStrategy(); - -WalkontableRowStrategy.prototype.add = function (i, TD, reverse) { - if (!this.isLastIncomplete()) { - var size = this.sizeAtIndex(i, TD); - if (size === void 0) { - return false; //total rows exceeded - } - var containerSize = this.getContainerSize(this.cellSizesSum + size); - if (reverse) { - this.cellSizes.unshift(size); - } - else { - this.cellSizes.push(size); - } - this.cellSizesSum += size; - this.cellCount++; - this.remainingSize = this.cellSizesSum - containerSize; - - if (reverse && this.isLastIncomplete()) { //something is outside of the screen, maybe even some full rows? - return false; - } - return true; - } - return false; -}; - -WalkontableRowStrategy.prototype.remove = function () { - var size = this.cellSizes.pop(); - this.cellSizesSum -= size; - this.cellCount--; - this.remainingSize -= size; -}; - -WalkontableRowStrategy.prototype.removeOutstanding = function () { - while (this.cellCount > 0 && this.cellSizes[this.cellCount - 1] < this.remainingSize) { //this row is completely off screen! - this.remove(); - } -}; -function WalkontableScroll(instance) { - this.instance = instance; -} - -WalkontableScroll.prototype.scrollVertical = function (delta) { - if (!this.instance.drawn) { - throw new Error('scrollVertical can only be called after table was drawn to DOM'); - } - - var instance = this.instance - , newOffset - , offset = instance.getSetting('offsetRow') - , fixedCount = instance.getSetting('fixedRowsTop') - , total = instance.getSetting('totalRows') - , maxSize = instance.wtViewport.getViewportHeight(); - - if (total > 0) { - newOffset = this.scrollLogicVertical(delta, offset, total, fixedCount, maxSize, function (row) { - if (row - offset < fixedCount && row - offset >= 0) { - return instance.getSetting('rowHeight', row - offset); - } - else { - return instance.getSetting('rowHeight', row); - } - }, function (isReverse) { - instance.wtTable.verticalRenderReverse = isReverse; - }); - } - else { - newOffset = 0; - } - - if (newOffset !== offset) { - this.instance.wtScrollbars.vertical.scrollTo(newOffset); - } - return instance; -}; - -WalkontableScroll.prototype.scrollHorizontal = function (delta) { - if (!this.instance.drawn) { - throw new Error('scrollHorizontal can only be called after table was drawn to DOM'); - } - - var instance = this.instance - , newOffset - , offset = instance.getSetting('offsetColumn') - , fixedCount = instance.getSetting('fixedColumnsLeft') - , total = instance.getSetting('totalColumns') - , maxSize = instance.wtViewport.getViewportWidth(); - - if (total > 0) { - newOffset = this.scrollLogicHorizontal(delta, offset, total, fixedCount, maxSize, function (col) { - if (col - offset < fixedCount && col - offset >= 0) { - return instance.getSetting('columnWidth', col - offset); - } - else { - return instance.getSetting('columnWidth', col); - } - }); - } - else { - newOffset = 0; - } - - if (newOffset !== offset) { - this.instance.wtScrollbars.horizontal.scrollTo(newOffset); - } - return instance; -}; - -WalkontableScroll.prototype.scrollLogicVertical = function (delta, offset, total, fixedCount, maxSize, cellSizeFn, setReverseRenderFn) { - var newOffset = offset + delta; - - if (newOffset >= total - fixedCount) { - newOffset = total - fixedCount - 1; - setReverseRenderFn(true); - } - - if (newOffset < 0) { - newOffset = 0; - } - - return newOffset; -}; - -WalkontableScroll.prototype.scrollLogicHorizontal = function (delta, offset, total, fixedCount, maxSize, cellSizeFn) { - var newOffset = offset + delta - , sum = 0 - , col; - - if (newOffset > fixedCount) { - if (newOffset >= total - fixedCount) { - newOffset = total - fixedCount - 1; - } - - col = newOffset; - while (sum < maxSize && col < total) { - sum += cellSizeFn(col); - col++; - } - - if (sum < maxSize) { - while (newOffset > 0) { - //if sum still less than available width, we cannot scroll that far (must move offset to the left) - sum += cellSizeFn(newOffset - 1); - if (sum < maxSize) { - newOffset--; - } - else { - break; - } - } - } - } - else if (newOffset < 0) { - newOffset = 0; - } - - return newOffset; -}; - -/** - * Scrolls viewport to a cell by minimum number of cells - */ -WalkontableScroll.prototype.scrollViewport = function (coords) { - var offsetRow = this.instance.getSetting('offsetRow') - , offsetColumn = this.instance.getSetting('offsetColumn') - , lastVisibleRow = this.instance.wtTable.getLastVisibleRow() - , totalRows = this.instance.getSetting('totalRows') - , totalColumns = this.instance.getSetting('totalColumns') - , fixedRowsTop = this.instance.getSetting('fixedRowsTop') - , fixedColumnsLeft = this.instance.getSetting('fixedColumnsLeft'); - - if (this.instance.isNativeScroll) { - var TD = this.instance.wtTable.getCell(coords); - if (typeof TD === 'object') { - var offset = WalkontableDom.prototype.offset(TD); - var outerHeight = WalkontableDom.prototype.outerHeight(TD); - var scrollY = window.scrollY; - var clientHeight = document.documentElement.clientHeight; - if (outerHeight < clientHeight) { - if (offset.top < scrollY) { - TD.scrollIntoView(true); - } - else if (offset.top + outerHeight > scrollY + clientHeight) { - TD.scrollIntoView(false); - } - } - return; - } - } - - if (coords[0] < 0 || coords[0] > totalRows - 1) { - throw new Error('row ' + coords[0] + ' does not exist'); - } - else if (coords[1] < 0 || coords[1] > totalColumns - 1) { - throw new Error('column ' + coords[1] + ' does not exist'); - } - - if (coords[0] > lastVisibleRow) { -// this.scrollVertical(coords[0] - lastVisibleRow + 1); - this.scrollVertical(coords[0] - fixedRowsTop - offsetRow); - this.instance.wtTable.verticalRenderReverse = true; - } - else if (coords[0] === lastVisibleRow && this.instance.wtTable.rowStrategy.isLastIncomplete()) { -// this.scrollVertical(coords[0] - lastVisibleRow + 1); - this.scrollVertical(coords[0] - fixedRowsTop - offsetRow); - this.instance.wtTable.verticalRenderReverse = true; - } - else if (coords[0] - fixedRowsTop < offsetRow) { - this.scrollVertical(coords[0] - fixedRowsTop - offsetRow); - } - else { - this.scrollVertical(0); //Craig's issue: remove row from the last scroll page should scroll viewport a row up if needed - } - - if (this.instance.wtTable.isColumnBeforeViewport(coords[1])) { - //scroll left - this.instance.wtScrollbars.horizontal.scrollTo(coords[1] - fixedColumnsLeft); - } - else if (this.instance.wtTable.isColumnAfterViewport(coords[1]) || (this.instance.wtTable.getLastVisibleColumn() === coords[1] && !this.instance.wtTable.isLastColumnFullyVisible())) { - //scroll right - var sum = 0; - for (var i = 0; i < fixedColumnsLeft; i++) { - sum += this.instance.getSetting('columnWidth', i); - } - var scrollTo = coords[1]; - sum += this.instance.getSetting('columnWidth', scrollTo); - var available = this.instance.wtViewport.getViewportWidth(); - if (sum < available) { - var next = this.instance.getSetting('columnWidth', scrollTo - 1); - while (sum + next < available && scrollTo >= fixedColumnsLeft) { - scrollTo--; - sum += next; - } - } - - this.instance.wtScrollbars.horizontal.scrollTo(scrollTo - fixedColumnsLeft); - } - /*else { - //no scroll - }*/ - - return this.instance; -}; - -function WalkontableScrollbar() { -} - -WalkontableScrollbar.prototype.init = function () { - var that = this; - - //reference to instance - this.$table = $(this.instance.wtTable.TABLE); - - //create elements - this.slider = document.createElement('DIV'); - this.sliderStyle = this.slider.style; - this.sliderStyle.position = 'absolute'; - this.sliderStyle.top = '0'; - this.sliderStyle.left = '0'; - this.sliderStyle.display = 'none'; - this.slider.className = 'dragdealer ' + this.type; - - this.handle = document.createElement('DIV'); - this.handleStyle = this.handle.style; - this.handle.className = 'handle'; - - this.slider.appendChild(this.handle); - this.container = this.instance.wtTable.holder; - this.container.appendChild(this.slider); - - var firstRun = true; - this.dragTimeout = null; - var dragDelta; - var dragRender = function () { - that.onScroll(dragDelta); - }; - - this.dragdealer = new Dragdealer(this.slider, { - vertical: (this.type === 'vertical'), - horizontal: (this.type === 'horizontal'), - slide: false, - speed: 100, - animationCallback: function (x, y) { - if (firstRun) { - firstRun = false; - return; - } - that.skipRefresh = true; - dragDelta = that.type === 'vertical' ? y : x; - if (that.dragTimeout === null) { - that.dragTimeout = setInterval(dragRender, 100); - dragRender(); - } - }, - callback: function (x, y) { - that.skipRefresh = false; - clearInterval(that.dragTimeout); - that.dragTimeout = null; - dragDelta = that.type === 'vertical' ? y : x; - that.onScroll(dragDelta); - } - }); - this.skipRefresh = false; -}; - -WalkontableScrollbar.prototype.onScroll = function (delta) { - if (this.instance.drawn) { - this.readSettings(); - if (this.total > this.visibleCount) { - var newOffset = Math.round(this.handlePosition * this.total / this.sliderSize); - - if (delta === 1) { - if (this.type === 'vertical') { - this.instance.scrollVertical(Infinity).draw(); - } - else { - this.instance.scrollHorizontal(Infinity).draw(); - } - } - else if (newOffset !== this.offset) { //is new offset different than old offset - if (this.type === 'vertical') { - this.instance.scrollVertical(newOffset - this.offset).draw(); - } - else { - this.instance.scrollHorizontal(newOffset - this.offset).draw(); - } - } - else { - this.refresh(); - } - } - } -}; - -/** - * Returns what part of the scroller should the handle take - * @param viewportCount {Number} number of visible rows or columns - * @param totalCount {Number} total number of rows or columns - * @return {Number} 0..1 - */ -WalkontableScrollbar.prototype.getHandleSizeRatio = function (viewportCount, totalCount) { - if (!totalCount || viewportCount > totalCount || viewportCount == totalCount) { - return 1; - } - return 1 / totalCount; -}; - -WalkontableScrollbar.prototype.prepare = function () { - if (this.skipRefresh) { - return; - } - var ratio = this.getHandleSizeRatio(this.visibleCount, this.total); - if (((ratio === 1 || isNaN(ratio)) && this.scrollMode === 'auto') || this.scrollMode === 'none') { - //isNaN is needed because ratio equals NaN when totalRows/totalColumns equals 0 - this.visible = false; - } - else { - this.visible = true; - } -}; - -WalkontableScrollbar.prototype.refresh = function () { - if (this.skipRefresh) { - return; - } - else if (!this.visible) { - this.sliderStyle.display = 'none'; - return; - } - - var ratio - , sliderSize - , handleSize - , handlePosition - , visibleCount = this.visibleCount - , tableWidth = this.instance.wtViewport.getWorkspaceWidth() - , tableHeight = this.instance.wtViewport.getWorkspaceHeight(); - - if (tableWidth === Infinity) { - tableWidth = this.instance.wtViewport.getWorkspaceActualWidth(); - } - - if (tableHeight === Infinity) { - tableHeight = this.instance.wtViewport.getWorkspaceActualHeight(); - } - - if (this.type === 'vertical') { - if (this.instance.wtTable.rowStrategy.isLastIncomplete()) { - visibleCount--; - } - - sliderSize = tableHeight - 2; //2 is sliders border-width - - this.sliderStyle.top = this.instance.wtDom.offset(this.$table[0]).top - this.instance.wtDom.offset(this.container).top + 'px'; - this.sliderStyle.left = tableWidth - 1 + 'px'; //1 is sliders border-width - this.sliderStyle.height = Math.max(sliderSize, 0) + 'px'; - } - else { //horizontal - sliderSize = tableWidth - 2; //2 is sliders border-width - - this.sliderStyle.left = this.instance.wtDom.offset(this.$table[0]).left - this.instance.wtDom.offset(this.container).left + 'px'; - this.sliderStyle.top = tableHeight - 1 + 'px'; //1 is sliders border-width - this.sliderStyle.width = Math.max(sliderSize, 0) + 'px'; - } - - ratio = this.getHandleSizeRatio(visibleCount, this.total); - handleSize = Math.round(sliderSize * ratio); - if (handleSize < 10) { - handleSize = 15; - } - - handlePosition = Math.floor(sliderSize * (this.offset / this.total)); - if (handleSize + handlePosition > sliderSize) { - handlePosition = sliderSize - handleSize; - } - - if (this.type === 'vertical') { - this.handleStyle.height = handleSize + 'px'; - this.handleStyle.top = handlePosition + 'px'; - - } - else { //horizontal - this.handleStyle.width = handleSize + 'px'; - this.handleStyle.left = handlePosition + 'px'; - } - - this.sliderStyle.display = 'block'; -}; - -WalkontableScrollbar.prototype.destroy = function () { - clearInterval(this.dragdealer.interval); -}; - -/// - -var WalkontableVerticalScrollbar = function (instance) { - this.instance = instance; - this.type = 'vertical'; - this.init(); -}; - -WalkontableVerticalScrollbar.prototype = new WalkontableScrollbar(); - -WalkontableVerticalScrollbar.prototype.scrollTo = function (cell) { - this.instance.update('offsetRow', cell); -}; - -WalkontableVerticalScrollbar.prototype.readSettings = function () { - this.scrollMode = this.instance.getSetting('scrollV'); - this.offset = this.instance.getSetting('offsetRow'); - this.total = this.instance.getSetting('totalRows'); - this.visibleCount = this.instance.wtTable.rowStrategy.countVisible(); - if(this.visibleCount > 1 && this.instance.wtTable.rowStrategy.isLastIncomplete()) { - this.visibleCount--; - } - this.handlePosition = parseInt(this.handleStyle.top, 10); - this.sliderSize = parseInt(this.sliderStyle.height, 10); - this.fixedCount = this.instance.getSetting('fixedRowsTop'); -}; - -/// - -var WalkontableHorizontalScrollbar = function (instance) { - this.instance = instance; - this.type = 'horizontal'; - this.init(); -}; - -WalkontableHorizontalScrollbar.prototype = new WalkontableScrollbar(); - -WalkontableHorizontalScrollbar.prototype.scrollTo = function (cell) { - this.instance.update('offsetColumn', cell); -}; - -WalkontableHorizontalScrollbar.prototype.readSettings = function () { - this.scrollMode = this.instance.getSetting('scrollH'); - this.offset = this.instance.getSetting('offsetColumn'); - this.total = this.instance.getSetting('totalColumns'); - this.visibleCount = this.instance.wtTable.columnStrategy.countVisible(); - if(this.visibleCount > 1 && this.instance.wtTable.columnStrategy.isLastIncomplete()) { - this.visibleCount--; - } - this.handlePosition = parseInt(this.handleStyle.left, 10); - this.sliderSize = parseInt(this.sliderStyle.width, 10); - this.fixedCount = this.instance.getSetting('fixedColumnsLeft'); -}; - -WalkontableHorizontalScrollbar.prototype.getHandleSizeRatio = function (viewportCount, totalCount) { - if (!totalCount || viewportCount > totalCount || viewportCount == totalCount) { - return 1; - } - return viewportCount / totalCount; -}; -function WalkontableScrollbarNative() { - this.lastWindowScrollPosition = NaN; - this.maxOuts = 10; //max outs in one direction (before and after table) -} - -WalkontableScrollbarNative.prototype.init = function () { - this.TABLE = this.instance.wtTable.TABLE; - this.fixed = this.instance.wtTable.hider; - this.fixedContainer = this.instance.wtTable.holder; - this.fixed.style.position = 'absolute'; - this.fixed.style.left = '0'; - this.$scrollHandler = $(window); //in future remove jQuery from here - - var that = this; - this.$scrollHandler.on('scroll.walkontable', function () { - if (!that.instance.wtTable.holder.parentNode) { - //Walkontable was detached from DOM, but this handler was not removed - that.destroy(); - return; - } - - that.onScroll(); - }); - - this.readSettings(); -}; - -WalkontableScrollbarNative.prototype.onScroll = function (forcePosition) { - this.readSettings(); //read window scroll position - if (forcePosition) { - - this.windowScrollPosition = forcePosition; - } - - if (this.windowScrollPosition === this.lastWindowScrollPosition) { - return; - } - this.lastWindowScrollPosition = this.windowScrollPosition; - - var scrollDelta; - var newOffset = 0; - - if (1 == 1 || this.windowScrollPosition > this.tableParentOffset) { - scrollDelta = this.windowScrollPosition - this.tableParentOffset; - - partialOffset = 0; - if (scrollDelta > 0) { - var sum = 0; - var last; - for (var i = 0; i < this.total; i++) { - last = this.instance.getSetting('rowHeight', i); - sum += last; - if (sum > scrollDelta) { - break; - } - } - - if (this.offset > 0) { - partialOffset = (sum - scrollDelta); - } - newOffset = i; - newOffset = Math.min(newOffset, this.total); - } - } - - this.curOuts = newOffset > this.maxOuts ? this.maxOuts : newOffset; - newOffset -= this.curOuts; - - this.instance.update('offsetRow', newOffset); - this.readSettings(); //read new offset - this.instance.draw(); -}; - -WalkontableScrollbarNative.prototype.prepare = function () { -}; - -WalkontableScrollbarNative.prototype.availableSize = function () { - var availableSize; - - if (this.windowScrollPosition > this.tableParentOffset /*&& last > -1*/) { //last -1 means that viewport is scrolled behind the table - if (this.instance.wtTable.getLastVisibleRow() === this.total - 1) { - availableSize = this.instance.wtDom.outerHeight(this.TABLE); - } - else { - availableSize = this.windowSize; - } - } - else { - availableSize = this.windowSize - (this.tableParentOffset - this.windowScrollPosition); - } - - return availableSize; -}; - -WalkontableScrollbarNative.prototype.refresh = function () { - var last = this.getLastCell(); - this.measureBefore = this.sumCellSizes(0, this.offset); - this.measureInside = this.getTableSize(); - if (last === -1) { //last -1 means that viewport is scrolled behind the table - this.measureAfter = 0; - } - else { - this.measureAfter = this.sumCellSizes(last, this.total - last); - } - this.applyToDOM(); -}; - -WalkontableScrollbarNative.prototype.destroy = function () { - this.$scrollHandler.off('scroll.walkontable'); -}; - -/// - -var WalkontableVerticalScrollbarNative = function (instance) { - this.instance = instance; - this.type = 'vertical'; - this.cellSize = 23; - this.init(); - - var that = this; - WalkontableCellStrategy.prototype.isLastIncomplete = function () { //monkey patch needed. In future get rid of it to improve performance - /* - * this.remainingSize = window viewport reduced by sum of all rendered cells (also those before the visible part) - * that.sumCellSizes(...) = sum of the sizes of cells that are before the visible part + 1 cell that is partially visible on top of the screen - */ - return this.remainingSize > that.sumCellSizes(that.offset, that.offset + that.curOuts + 1); - }; -}; - -WalkontableVerticalScrollbarNative.prototype = new WalkontableScrollbarNative(); - -WalkontableVerticalScrollbarNative.prototype.getLastCell = function () { - return this.instance.getSetting('offsetRow') + this.instance.wtTable.tbodyChildrenLength - 1; -}; - -WalkontableVerticalScrollbarNative.prototype.getTableSize = function () { - return this.instance.wtDom.outerHeight(this.TABLE); -}; - -var partialOffset = 0; - -WalkontableVerticalScrollbarNative.prototype.sumCellSizes = function (from, length) { - var sum = 0; - while (from < length) { - sum += this.instance.getSetting('rowHeight', from); - from++; - } - return sum; -}; - -WalkontableVerticalScrollbarNative.prototype.applyToDOM = function () { - var headerSize = this.instance.wtViewport.getColumnHeaderHeight(); - this.fixedContainer.style.height = headerSize + this.sumCellSizes(0, this.total) + 'px'; - this.fixed.style.top = this.measureBefore + 'px'; - this.fixed.style.bottom = ''; -}; - -WalkontableVerticalScrollbarNative.prototype.scrollTo = function (cell) { - var newY = this.tableParentOffset + cell * this.cellSize; - this.$scrollHandler.scrollTop(newY); - this.onScroll(newY); -}; - -WalkontableVerticalScrollbarNative.prototype.readSettings = function () { - var offset = this.instance.wtDom.offset(this.fixedContainer); - this.tableParentOffset = offset.top; - this.tableParentOtherOffset = offset.left; - this.windowSize = this.$scrollHandler.height(); - this.windowScrollPosition = this.$scrollHandler.scrollTop(); - this.offset = this.instance.getSetting('offsetRow'); - this.total = this.instance.getSetting('totalRows'); -}; - -/// - -var WalkontableHorizontalScrollbarNative = function (instance) { - this.instance = instance; - this.type = 'horizontal'; - this.cellSize = 50; - this.init(); -}; - -WalkontableHorizontalScrollbarNative.prototype = new WalkontableScrollbarNative(); - -WalkontableHorizontalScrollbarNative.prototype.getLastCell = function () { - return this.instance.wtTable.getLastVisibleColumn(); -}; - -WalkontableHorizontalScrollbarNative.prototype.getTableSize = function () { - return this.instance.wtDom.outerWidth(this.TABLE); -}; - -WalkontableHorizontalScrollbarNative.prototype.applyToDOM = function () { - this.fixedContainer.style.paddingLeft = this.measureBefore + 'px'; - this.fixedContainer.style.paddingRight = this.measureAfter + 'px'; -}; - -WalkontableHorizontalScrollbarNative.prototype.scrollTo = function (cell) { - this.$scrollHandler.scrollLeft(this.tableParentOffset + cell * this.cellSize); -}; - -WalkontableHorizontalScrollbarNative.prototype.readSettings = function () { - var offset = this.instance.wtDom.offset(this.fixedContainer); - this.tableParentOffset = offset.left; - this.tableParentOtherOffset = offset.top; - this.windowSize = this.$scrollHandler.width(); - this.windowScrollPosition = this.$scrollHandler.scrollLeft(); - this.offset = this.instance.getSetting('offsetColumn'); - this.total = this.instance.getSetting('totalColumns'); -}; -function WalkontableScrollbars(instance) { - if(instance.getSetting('scrollbarModelV') === 'native') { - instance.update('scrollbarModelH', 'none'); - } - - switch (instance.getSetting('scrollbarModelV')) { - case 'dragdealer': - this.vertical = new WalkontableVerticalScrollbar(instance); - break; - - case 'native': - this.vertical = new WalkontableVerticalScrollbarNative(instance); - break; - } - - switch (instance.getSetting('scrollbarModelH')) { - case 'dragdealer': - this.horizontal = new WalkontableHorizontalScrollbar(instance); - break; - - case 'native': - this.horizontal = new WalkontableHorizontalScrollbarNative(instance); - break; - } -} - -WalkontableScrollbars.prototype.destroy = function () { - this.vertical && this.vertical.destroy(); - this.horizontal && this.horizontal.destroy(); -}; - -WalkontableScrollbars.prototype.refresh = function () { - this.horizontal && this.horizontal.readSettings(); - this.vertical && this.vertical.readSettings(); - this.horizontal && this.horizontal.prepare(); - this.vertical && this.vertical.prepare(); - this.horizontal && this.horizontal.refresh(); - this.vertical && this.vertical.refresh(); -}; -function WalkontableSelection(instance, settings) { - this.instance = instance; - this.settings = settings; - this.selected = []; - if (settings.border) { - this.border = new WalkontableBorder(instance, settings); - } -} - -WalkontableSelection.prototype.add = function (coords) { - this.selected.push(coords); -}; - -WalkontableSelection.prototype.clear = function () { - this.selected.length = 0; //http://jsperf.com/clear-arrayxxx -}; - -/** - * Returns the top left (TL) and bottom right (BR) selection coordinates - * @returns {Object} - */ -WalkontableSelection.prototype.getCorners = function () { - var minRow - , minColumn - , maxRow - , maxColumn - , i - , ilen = this.selected.length; - - if (ilen > 0) { - minRow = maxRow = this.selected[0][0]; - minColumn = maxColumn = this.selected[0][1]; - - if (ilen > 1) { - for (i = 1; i < ilen; i++) { - if (this.selected[i][0] < minRow) { - minRow = this.selected[i][0]; - } - else if (this.selected[i][0] > maxRow) { - maxRow = this.selected[i][0]; - } - - if (this.selected[i][1] < minColumn) { - minColumn = this.selected[i][1]; - } - else if (this.selected[i][1] > maxColumn) { - maxColumn = this.selected[i][1]; - } - } - } - } - - return [minRow, minColumn, maxRow, maxColumn]; -}; - -WalkontableSelection.prototype.draw = function () { - var corners, r, c, source_r, source_c; - - var visibleRows = this.instance.wtTable.rowStrategy.countVisible() - , visibleColumns = this.instance.wtTable.columnStrategy.countVisible(); - - if (this.selected.length) { - corners = this.getCorners(); - - for (r = 0; r < visibleRows; r++) { - for (c = 0; c < visibleColumns; c++) { - source_r = this.instance.wtTable.rowFilter.visibleToSource(r); - source_c = this.instance.wtTable.columnFilter.visibleToSource(c); - - if (source_r >= corners[0] && source_r <= corners[2] && source_c >= corners[1] && source_c <= corners[3]) { - //selected cell - this.instance.wtTable.currentCellCache.add(r, c, this.settings.className); - } - else if (source_r >= corners[0] && source_r <= corners[2]) { - //selection is in this row - this.instance.wtTable.currentCellCache.add(r, c, this.settings.highlightRowClassName); - } - else if (source_c >= corners[1] && source_c <= corners[3]) { - //selection is in this column - this.instance.wtTable.currentCellCache.add(r, c, this.settings.highlightColumnClassName); - } - } - } - - this.border && this.border.appear(corners); //warning! border.appear modifies corners! - } - else { - this.border && this.border.disappear(); - } -}; - -function WalkontableSettings(instance, settings) { - var that = this; - this.instance = instance; - - //default settings. void 0 means it is required, null means it can be empty - this.defaults = { - table: void 0, - - //presentation mode - scrollH: 'auto', //values: scroll (always show scrollbar), auto (show scrollbar if table does not fit in the container), none (never show scrollbar) - scrollV: 'auto', //values: see above - scrollbarModelH: 'dragdealer', //values: dragdealer, native - scrollbarModelV: 'dragdealer', //values: dragdealer, native - stretchH: 'hybrid', //values: hybrid, all, last, none - currentRowClassName: null, - currentColumnClassName: null, - - //data source - data: void 0, - offsetRow: 0, - offsetColumn: 0, - fixedColumnsLeft: 0, - fixedRowsTop: 0, - rowHeaders: function () { - return [] - }, //this must be array of functions: [function (row, TH) {}] - columnHeaders: function () { - return [] - }, //this must be array of functions: [function (column, TH) {}] - totalRows: void 0, - totalColumns: void 0, - width: null, - height: null, - cellRenderer: function (row, column, TD) { - var cellData = that.getSetting('data', row, column); - that.instance.wtDom.fastInnerText(TD, cellData === void 0 || cellData === null ? '' : cellData); - }, - columnWidth: 50, - selections: null, - hideBorderOnMouseDownOver: false, - - //callbacks - onCellMouseDown: null, - onCellMouseOver: null, -// onCellMouseOut: null, - onCellDblClick: null, - onCellCornerMouseDown: null, - onCellCornerDblClick: null, - beforeDraw: null, - onDraw: null, - - //constants - scrollbarWidth: 10, - scrollbarHeight: 10 - }; - - //reference to settings - this.settings = {}; - for (var i in this.defaults) { - if (this.defaults.hasOwnProperty(i)) { - if (settings[i] !== void 0) { - this.settings[i] = settings[i]; - } - else if (this.defaults[i] === void 0) { - throw new Error('A required setting "' + i + '" was not provided'); - } - else { - this.settings[i] = this.defaults[i]; - } - } - } -} - -/** - * generic methods - */ - -WalkontableSettings.prototype.update = function (settings, value) { - if (value === void 0) { //settings is object - for (var i in settings) { - if (settings.hasOwnProperty(i)) { - this.settings[i] = settings[i]; - } - } - } - else { //if value is defined then settings is the key - this.settings[settings] = value; - } - return this.instance; -}; - -WalkontableSettings.prototype.getSetting = function (key, param1, param2, param3) { - if (this[key]) { - return this[key](param1, param2, param3); - } - else { - return this._getSetting(key, param1, param2, param3); - } -}; - -WalkontableSettings.prototype._getSetting = function (key, param1, param2, param3) { - if (typeof this.settings[key] === 'function') { - return this.settings[key](param1, param2, param3); - } - else if (param1 !== void 0 && Object.prototype.toString.call(this.settings[key]) === '[object Array]') { - return this.settings[key][param1]; - } - else { - return this.settings[key]; - } -}; - -WalkontableSettings.prototype.has = function (key) { - return !!this.settings[key] -}; - -/** - * specific methods - */ -WalkontableSettings.prototype.rowHeight = function (row, TD) { - if (!this.instance.rowHeightCache) { - this.instance.rowHeightCache = []; //hack. This cache is being invalidated in WOT core.js - } - if (this.instance.rowHeightCache[row] === void 0) { - var size = 23; //guess - if (TD) { - size = this.instance.wtDom.outerHeight(TD); //measure - this.instance.rowHeightCache[row] = size; //cache only something we measured - } - return size; - } - else { - return this.instance.rowHeightCache[row]; - } -}; -function WalkontableTable(instance) { - //reference to instance - this.instance = instance; - this.TABLE = this.instance.getSetting('table'); - this.wtDom = this.instance.wtDom; - this.wtDom.removeTextNodes(this.TABLE); - - //wtSpreader - var parent = this.TABLE.parentNode; - if (!parent || parent.nodeType !== 1 || !this.wtDom.hasClass(parent, 'wtHolder')) { - var spreader = document.createElement('DIV'); - spreader.className = 'wtSpreader'; - if (parent) { - parent.insertBefore(spreader, this.TABLE); //if TABLE is detached (e.g. in Jasmine test), it has no parentNode so we cannot attach holder to it - } - spreader.appendChild(this.TABLE); - } - this.spreader = this.TABLE.parentNode; - - //wtHider - parent = this.spreader.parentNode; - if (!parent || parent.nodeType !== 1 || !this.wtDom.hasClass(parent, 'wtHolder')) { - var hider = document.createElement('DIV'); - hider.className = 'wtHider'; - if (parent) { - parent.insertBefore(hider, this.spreader); //if TABLE is detached (e.g. in Jasmine test), it has no parentNode so we cannot attach holder to it - } - hider.appendChild(this.spreader); - } - this.hider = this.spreader.parentNode; - this.hiderStyle = this.hider.style; - this.hiderStyle.position = 'relative'; - - //wtHolder - parent = this.hider.parentNode; - if (!parent || parent.nodeType !== 1 || !this.wtDom.hasClass(parent, 'wtHolder')) { - var holder = document.createElement('DIV'); - holder.style.position = 'relative'; - holder.className = 'wtHolder'; - if (parent) { - parent.insertBefore(holder, this.hider); //if TABLE is detached (e.g. in Jasmine test), it has no parentNode so we cannot attach holder to it - } - holder.appendChild(this.hider); - } - this.holder = this.hider.parentNode; - - //bootstrap from settings - this.TBODY = this.TABLE.getElementsByTagName('TBODY')[0]; - if (!this.TBODY) { - this.TBODY = document.createElement('TBODY'); - this.TABLE.appendChild(this.TBODY); - } - this.THEAD = this.TABLE.getElementsByTagName('THEAD')[0]; - if (!this.THEAD) { - this.THEAD = document.createElement('THEAD'); - this.TABLE.insertBefore(this.THEAD, this.TBODY); - } - this.COLGROUP = this.TABLE.getElementsByTagName('COLGROUP')[0]; - if (!this.COLGROUP) { - this.COLGROUP = document.createElement('COLGROUP'); - this.TABLE.insertBefore(this.COLGROUP, this.THEAD); - } - - if (this.instance.getSetting('columnHeaders').length) { - if (!this.THEAD.childNodes.length) { - var TR = document.createElement('TR'); - this.THEAD.appendChild(TR); - } - } - - this.colgroupChildrenLength = this.COLGROUP.childNodes.length; - this.theadChildrenLength = this.THEAD.firstChild ? this.THEAD.firstChild.childNodes.length : 0; - this.tbodyChildrenLength = this.TBODY.childNodes.length; - - this.oldCellCache = new WalkontableClassNameCache(); - this.currentCellCache = new WalkontableClassNameCache(); - - this.rowFilter = new WalkontableRowFilter(); - this.columnFilter = new WalkontableColumnFilter(); - - this.verticalRenderReverse = false; - - if (this.instance.getSetting('scrollbarModelV') === 'native' || this.instance.getSetting('scrollbarModelH') === 'native') { - this.instance.isNativeScroll = true; - } -} - -WalkontableTable.prototype.refreshHiderDimensions = function () { - var height = this.instance.wtViewport.getWorkspaceHeight(); - var width = this.instance.wtViewport.getWorkspaceWidth(); - - var spreaderStyle = this.spreader.style; - - if ((height !== Infinity || width !== Infinity) && !this.instance.isNativeScroll) { - if (height === Infinity) { - height = this.instance.wtViewport.getWorkspaceActualHeight(); - } - if (width === Infinity) { - width = this.instance.wtViewport.getWorkspaceActualWidth(); - } - - this.hiderStyle.overflow = 'hidden'; - - spreaderStyle.position = 'absolute'; - spreaderStyle.top = '0'; - spreaderStyle.left = '0'; - - if (this.instance.getSetting('scrollbarModelV') === 'dragdealer') { - spreaderStyle.height = '4000px'; - } - - if (this.instance.getSetting('scrollbarModelH') === 'dragdealer') { - spreaderStyle.width = '4000px'; - } - - if (height < 0) { //this happens with WalkontableScrollbarNative and causes "Invalid argument" error in IE8 - height = 0; - } - - this.hiderStyle.height = height + 'px'; - this.hiderStyle.width = width + 'px'; - } - else { - spreaderStyle.position = 'relative'; - spreaderStyle.width = 'auto'; - spreaderStyle.height = 'auto'; - } -}; - -WalkontableTable.prototype.refreshStretching = function () { - var instance = this.instance - , stretchH = instance.getSetting('stretchH') - , totalRows = instance.getSetting('totalRows') - , totalColumns = instance.getSetting('totalColumns') - , offsetColumn = instance.getSetting('offsetColumn'); - - var containerWidthFn = function (cacheWidth) { - return that.instance.wtViewport.getViewportWidth(cacheWidth); - }; - - var that = this; - - var columnWidthFn = function (i) { - var source_c = that.columnFilter.visibleToSource(i); - if (source_c < totalColumns) { - return instance.getSetting('columnWidth', source_c); - } - }; - - if (stretchH === 'hybrid') { - if (offsetColumn > 0) { - stretchH = 'last'; - } - else { - stretchH = 'none'; - } - } - - var containerHeightFn = function (cacheHeight) { - if (that.instance.isNativeScroll) { - return 2 * that.instance.wtViewport.getViewportHeight(cacheHeight); - } - return that.instance.wtViewport.getViewportHeight(cacheHeight); - }; - - var rowHeightFn = function (i, TD) { - var source_r = that.rowFilter.visibleToSource(i); - if (source_r < totalRows) { - if (that.verticalRenderReverse && i === 0) { - return that.instance.getSetting('rowHeight', source_r, TD) - 1; - } - else { - return that.instance.getSetting('rowHeight', source_r, TD); - } - } - }; - - this.columnStrategy = new WalkontableColumnStrategy(containerWidthFn, columnWidthFn, stretchH); - this.rowStrategy = new WalkontableRowStrategy(containerHeightFn, rowHeightFn); -}; - -WalkontableTable.prototype.adjustAvailableNodes = function () { - var displayTds - , rowHeaders = this.instance.getSetting('rowHeaders') - , displayThs = rowHeaders.length - , columnHeaders = this.instance.getSetting('columnHeaders') - , TR - , TD - , c; - - //adjust COLGROUP - while (this.colgroupChildrenLength < displayThs) { - this.COLGROUP.appendChild(document.createElement('COL')); - this.colgroupChildrenLength++; - } - - this.refreshStretching(); - displayTds = this.columnStrategy.cellCount; - - //adjust COLGROUP - while (this.colgroupChildrenLength < displayTds + displayThs) { - this.COLGROUP.appendChild(document.createElement('COL')); - this.colgroupChildrenLength++; - } - while (this.colgroupChildrenLength > displayTds + displayThs) { - this.COLGROUP.removeChild(this.COLGROUP.lastChild); - this.colgroupChildrenLength--; - } - - //adjust THEAD - TR = this.THEAD.firstChild; - if (columnHeaders.length) { - if (!TR) { - TR = document.createElement('TR'); - this.THEAD.appendChild(TR); - } - - this.theadChildrenLength = TR.childNodes.length; - while (this.theadChildrenLength < displayTds + displayThs) { - TR.appendChild(document.createElement('TH')); - this.theadChildrenLength++; - } - while (this.theadChildrenLength > displayTds + displayThs) { - TR.removeChild(TR.lastChild); - this.theadChildrenLength--; - } - } - else if (TR) { - this.wtDom.empty(TR); - } - - //draw COLGROUP - for (c = 0; c < this.colgroupChildrenLength; c++) { - if (c < displayThs) { - this.wtDom.addClass(this.COLGROUP.childNodes[c], 'rowHeader'); - } - else { - this.wtDom.removeClass(this.COLGROUP.childNodes[c], 'rowHeader'); - } - } - - //draw THEAD - if (columnHeaders.length) { - TR = this.THEAD.firstChild; - if (displayThs) { - TD = TR.firstChild; //actually it is TH but let's reuse single variable - for (c = 0; c < displayThs; c++) { - rowHeaders[c](-displayThs + c, TD); - TD = TD.nextSibling; - } - } - } - - for (c = 0; c < displayTds; c++) { - if (columnHeaders.length) { - columnHeaders[0](this.columnFilter.visibleToSource(c), TR.childNodes[displayThs + c]); - } - } -}; - -WalkontableTable.prototype.adjustColumns = function (TR, desiredCount) { - var count = TR.childNodes.length; - while (count < desiredCount) { - var TD = document.createElement('TD'); - TR.appendChild(TD); - count++; - } - while (count > desiredCount) { - TR.removeChild(TR.lastChild); - count--; - } -}; - -WalkontableTable.prototype.draw = function (selectionsOnly) { - if (this.instance.isNativeScroll) { - this.verticalRenderReverse = false; //this is only supported in dragdealer mode, not in native - } - - this.rowFilter.readSettings(this.instance); - this.columnFilter.readSettings(this.instance); - - if (!selectionsOnly) { - this.tableOffset = this.wtDom.offset(this.TABLE); - this._doDraw(); - } - else { - this.instance.wtScrollbars.refresh(); - } - - this.refreshPositions(selectionsOnly); - - this.instance.drawn = true; - return this; -}; - -WalkontableTable.prototype._doDraw = function () { - var r = 0 - , source_r - , c - , source_c - , offsetRow = this.instance.getSetting('offsetRow') - , totalRows = this.instance.getSetting('totalRows') - , totalColumns = this.instance.getSetting('totalColumns') - , displayTds - , rowHeaders = this.instance.getSetting('rowHeaders') - , displayThs = rowHeaders.length - , TR - , TD - , TH - , adjusted = false - , workspaceWidth - , mustBeInViewport - , res; - - if (this.verticalRenderReverse) { - mustBeInViewport = offsetRow; - } - - this.instance.wtViewport.resetSettings(); - - var noPartial = false; - if (this.verticalRenderReverse) { - if (offsetRow === totalRows - this.rowFilter.fixedCount - 1) { - noPartial = true; - } - else { - this.instance.update('offsetRow', offsetRow + 1); //if we are scrolling reverse - this.rowFilter.readSettings(this.instance); - } - } - - //draw TBODY - if (totalColumns > 0) { - source_r = this.rowFilter.visibleToSource(r); - - var first = true; - - while (source_r < totalRows && source_r >= 0) { - if (r > 1000) { - throw new Error('Security brake: Too much TRs. Please define height for your table, which will enforce scrollbars.'); - } - - if (r >= this.tbodyChildrenLength || (this.verticalRenderReverse && r >= this.rowFilter.fixedCount)) { - TR = document.createElement('TR'); - for (c = 0; c < displayThs; c++) { - TR.appendChild(document.createElement('TH')); - } - if (this.verticalRenderReverse && r >= this.rowFilter.fixedCount) { - this.TBODY.insertBefore(TR, this.TBODY.childNodes[this.rowFilter.fixedCount] || this.TBODY.firstChild); - } - else { - this.TBODY.appendChild(TR); - } - this.tbodyChildrenLength++; - } - else if (r === 0) { - TR = this.TBODY.firstChild; - } - else { - TR = TR.nextSibling; //http://jsperf.com/nextsibling-vs-indexed-childnodes - } - - //TH - TH = TR.firstChild; - for (c = 0; c < displayThs; c++) { - - //If the number of row headers increased we need to replace TD with TH - if (TH.nodeName == 'TD') { - TD = TH; - TH = document.createElement('TH'); - TR.insertBefore(TH, TD); - TR.removeChild(TD); - } - - rowHeaders[c](source_r, TH); //actually TH - TH = TH.nextSibling; //http://jsperf.com/nextsibling-vs-indexed-childnodes - } - - if (first) { -// if (r === 0) { - first = false; - - this.adjustAvailableNodes(); - adjusted = true; - displayTds = this.columnStrategy.cellCount; - - //TD - this.adjustColumns(TR, displayTds + displayThs); - - workspaceWidth = this.instance.wtViewport.getWorkspaceWidth(); - this.columnStrategy.stretch(); - for (c = 0; c < displayTds; c++) { - this.COLGROUP.childNodes[c + displayThs].style.width = this.columnStrategy.getSize(c) + 'px'; - } - } - else { - //TD - this.adjustColumns(TR, displayTds + displayThs); - } - - for (c = 0; c < displayTds; c++) { - source_c = this.columnFilter.visibleToSource(c); - if (c === 0) { - TD = TR.childNodes[this.columnFilter.sourceColumnToVisibleRowHeadedColumn(source_c)]; - } - else { - TD = TD.nextSibling; //http://jsperf.com/nextsibling-vs-indexed-childnodes - } - - //If the number of headers has been reduced, we need to replace excess TH with TD - if (TD.nodeName == 'TH') { - TH = TD; - TD = document.createElement('TD'); - TR.insertBefore(TD, TH); - TR.removeChild(TH); - } - - TD.className = ''; - TD.removeAttribute('style'); - this.instance.getSetting('cellRenderer', source_r, source_c, TD); - TD.setAttribute('data-row', source_r); - TD.setAttribute('data-column', source_c); - } - - offsetRow = this.instance.getSetting('offsetRow'); //refresh the value - - //after last column is rendered, check if last cell is fully displayed - if (this.verticalRenderReverse && noPartial) { - if (-this.wtDom.outerHeight(TR.firstChild) < this.rowStrategy.remainingSize) { - this.TBODY.removeChild(TR); - this.instance.update('offsetRow', offsetRow + 1); - this.tbodyChildrenLength--; - this.rowFilter.readSettings(this.instance); - break; - - } - else { - res = this.rowStrategy.add(r, TD, this.verticalRenderReverse); - if (res === false) { - this.rowStrategy.removeOutstanding(); - } - } - } - else { - res = this.rowStrategy.add(r, TD, this.verticalRenderReverse); - - if (res === false) { - if (!this.instance.isNativeScroll) { - this.rowStrategy.removeOutstanding(); - } - } - - if (this.rowStrategy.isLastIncomplete()) { - if (this.verticalRenderReverse && !this.isRowInViewport(mustBeInViewport)) { - //we failed because one of the cells was by far too large. Recover by rendering from top - this.verticalRenderReverse = false; - this.instance.update('offsetRow', mustBeInViewport); - this.draw(); - return; - } - break; - } - } - - if (this.verticalRenderReverse && r >= this.rowFilter.fixedCount) { - if (offsetRow === 0) { - break; - } - this.instance.update('offsetRow', offsetRow - 1); - this.rowFilter.readSettings(this.instance); - } - else { - r++; - } - - source_r = this.rowFilter.visibleToSource(r); - } - } - - if (!adjusted) { - this.adjustAvailableNodes(); - } - - r = this.rowStrategy.countVisible(); - while (this.tbodyChildrenLength > r) { - this.TBODY.removeChild(this.TBODY.lastChild); - this.tbodyChildrenLength--; - } - - this.instance.wtScrollbars.refresh(); - - if (workspaceWidth !== this.instance.wtViewport.getWorkspaceWidth()) { - //workspace width changed though to shown/hidden vertical scrollbar. Let's reapply stretching - this.columnStrategy.stretch(); - for (c = 0; c < this.columnStrategy.cellCount; c++) { - this.COLGROUP.childNodes[c + displayThs].style.width = this.columnStrategy.getSize(c) + 'px'; - } - } - - this.verticalRenderReverse = false; -}; - -WalkontableTable.prototype.refreshPositions = function (selectionsOnly) { - this.refreshHiderDimensions(); - this.refreshSelections(selectionsOnly); -}; - -WalkontableTable.prototype.refreshSelections = function (selectionsOnly) { - var vr - , r - , vc - , c - , s - , slen - , classNames = [] - , visibleRows = this.rowStrategy.countVisible() - , visibleColumns = this.columnStrategy.countVisible(); - - this.oldCellCache = this.currentCellCache; - this.currentCellCache = new WalkontableClassNameCache(); - - if (this.instance.selections) { - for (r in this.instance.selections) { - if (this.instance.selections.hasOwnProperty(r)) { - this.instance.selections[r].draw(); - if (this.instance.selections[r].settings.className) { - classNames.push(this.instance.selections[r].settings.className); - } - if (this.instance.selections[r].settings.highlightRowClassName) { - classNames.push(this.instance.selections[r].settings.highlightRowClassName); - } - if (this.instance.selections[r].settings.highlightColumnClassName) { - classNames.push(this.instance.selections[r].settings.highlightColumnClassName); - } - } - } - } - - slen = classNames.length; - - for (vr = 0; vr < visibleRows; vr++) { - for (vc = 0; vc < visibleColumns; vc++) { - r = this.rowFilter.visibleToSource(vr); - c = this.columnFilter.visibleToSource(vc); - for (s = 0; s < slen; s++) { - if (this.currentCellCache.test(vr, vc, classNames[s])) { - this.wtDom.addClass(this.getCell([r, c]), classNames[s]); - } - else if (selectionsOnly && this.oldCellCache.test(vr, vc, classNames[s])) { - this.wtDom.removeClass(this.getCell([r, c]), classNames[s]); - } - } - } - } -}; - -/** - * getCell - * @param {Array} coords - * @return {Object} HTMLElement on success or {Number} one of the exit codes on error: - * -1 row before viewport - * -2 row after viewport - * -3 column before viewport - * -4 column after viewport - * - */ -WalkontableTable.prototype.getCell = function (coords) { - if (this.instance.isNativeScroll) { - return this.instance.wtTable.TBODY.querySelectorAll('[data-row="' + coords[0] + '"][data-column="' + coords[1] + '"]')[0]; - } - - if (this.isRowBeforeViewport(coords[0])) { - return -1; //row before viewport - } - else if (this.isRowAfterViewport(coords[0])) { - return -2; //row after viewport - } - else { - if (this.isColumnBeforeViewport(coords[1])) { - return -3; //column before viewport - } - else if (this.isColumnAfterViewport(coords[1])) { - return -4; //column after viewport - } - else { - return this.TBODY.childNodes[this.rowFilter.sourceToVisible(coords[0])].childNodes[this.columnFilter.sourceColumnToVisibleRowHeadedColumn(coords[1])]; - } - } -}; - -WalkontableTable.prototype.getCoords = function (TD) { - return [ - this.rowFilter.visibleToSource(this.wtDom.index(TD.parentNode)), - this.columnFilter.visibleRowHeadedColumnToSourceColumn(TD.cellIndex) - ]; -}; - -//returns -1 if no row is visible -WalkontableTable.prototype.getLastVisibleRow = function () { - return this.rowFilter.visibleToSource(this.rowStrategy.cellCount - 1); -}; - -//returns -1 if no column is visible -WalkontableTable.prototype.getLastVisibleColumn = function () { - return this.columnFilter.visibleToSource(this.columnStrategy.cellCount - 1); -}; - -WalkontableTable.prototype.isRowBeforeViewport = function (r) { - return (this.rowFilter.sourceToVisible(r) < this.rowFilter.fixedCount && r >= this.rowFilter.fixedCount); -}; - -WalkontableTable.prototype.isRowAfterViewport = function (r) { - return (r > this.getLastVisibleRow()); -}; - -WalkontableTable.prototype.isColumnBeforeViewport = function (c) { - return (this.columnFilter.sourceToVisible(c) < this.columnFilter.fixedCount && c >= this.columnFilter.fixedCount); -}; - -WalkontableTable.prototype.isColumnAfterViewport = function (c) { - return (c > this.getLastVisibleColumn()); -}; - -WalkontableTable.prototype.isRowInViewport = function (r) { - if (this.instance.isNativeScroll) { - return !!this.instance.wtTable.TBODY.querySelectorAll('[data-row="' + r + '"]')[0]; - } - else { - return (!this.isRowBeforeViewport(r) && !this.isRowAfterViewport(r)); - } -}; - -WalkontableTable.prototype.isColumnInViewport = function (c) { - if (this.instance.isNativeScroll) { - return !!this.instance.wtTable.TBODY.querySelectorAll('[data-column="' + c + '"]')[0]; - } - else { - return (!this.isColumnBeforeViewport(c) && !this.isColumnAfterViewport(c)); - } -}; - -WalkontableTable.prototype.isLastRowFullyVisible = function () { - return (this.getLastVisibleRow() === this.instance.getSetting('totalRows') - 1 && !this.rowStrategy.isLastIncomplete()); -}; - -WalkontableTable.prototype.isLastColumnFullyVisible = function () { - return (this.getLastVisibleColumn() === this.instance.getSetting('totalColumns') - 1 && !this.columnStrategy.isLastIncomplete()); -}; - -function WalkontableViewport(instance) { - this.instance = instance; - this.resetSettings(); - - if (this.instance.isNativeScroll) { - var that = this; - that.clientHeight = document.documentElement.clientHeight; //browser viewport height - $(window).on('resize', function () { - that.clientHeight = document.documentElement.clientHeight; - }); - } -} - -/*WalkontableViewport.prototype.isInSightVertical = function () { - //is table outside viewport bottom edge - if (tableTop > windowHeight + scrollTop) { - return -1; - } - - //is table outside viewport top edge - else if (scrollTop > tableTop + tableFakeHeight) { - return -2; - } - - //table is in viewport but how much exactly? - else { - - } -};*/ - -//used by scrollbar -WalkontableViewport.prototype.getWorkspaceHeight = function (proposedHeight) { - if (this.instance.isNativeScroll) { - return this.clientHeight; - } - - var height = this.instance.getSetting('height'); - - if (height === Infinity || height === void 0 || height === null || height < 1) { - if (this.instance.wtScrollbars.vertical instanceof WalkontableScrollbarNative) { - height = this.instance.wtScrollbars.vertical.availableSize(); - } - else { - height = Infinity; - } - } - - if (height !== Infinity) { - if (proposedHeight >= height) { - height -= this.instance.getSetting('scrollbarHeight'); - } - else if (this.instance.wtScrollbars.horizontal.visible) { - height -= this.instance.getSetting('scrollbarHeight'); - } - } - - return height; -}; - -WalkontableViewport.prototype.getWorkspaceWidth = function (proposedWidth) { - var width = this.instance.getSetting('width'); - - if (width === Infinity || width === void 0 || width === null || width < 1) { - if (this.instance.wtScrollbars.horizontal instanceof WalkontableScrollbarNative) { - width = this.instance.wtScrollbars.horizontal.availableSize(); - } - else { - width = Infinity; - } - } - - if (width !== Infinity) { - if (proposedWidth >= width) { - width -= this.instance.getSetting('scrollbarWidth'); - } - else if (this.instance.wtScrollbars.vertical.visible) { - width -= this.instance.getSetting('scrollbarWidth'); - } - } - return width; -}; - -WalkontableViewport.prototype.getWorkspaceActualHeight = function () { - return this.instance.wtDom.outerHeight(this.instance.wtTable.TABLE); -}; - -WalkontableViewport.prototype.getWorkspaceActualWidth = function () { - return this.instance.wtDom.outerWidth(this.instance.wtTable.TABLE) || this.instance.wtDom.outerWidth(this.instance.wtTable.TBODY) || this.instance.wtDom.outerWidth(this.instance.wtTable.THEAD); //IE8 reports 0 as
    in TABLE offset (see also WalkontableDom.prototype.outerHeight) - //http://jsperf.com/offset-vs-getboundingclientrect/8 - var box = elem.getBoundingClientRect(); - return { - top: box.top + (window.pageYOffset || document.documentElement.scrollTop) - (document.documentElement.clientTop || 0), - left: box.left + (window.pageXOffset || document.documentElement.scrollLeft) - (document.documentElement.clientLeft || 0) - }; - } - - var offsetLeft = elem.offsetLeft - , offsetTop = elem.offsetTop - , lastElem = elem; - - while (elem = elem.offsetParent) { - if (elem === document.body) { //from my observation, document.body always has scrollLeft/scrollTop == 0 - break; - } - offsetLeft += elem.offsetLeft; - offsetTop += elem.offsetTop; - lastElem = elem; - } - - if (lastElem && lastElem.style.position === 'fixed') { //slow - http://jsperf.com/offset-vs-getboundingclientrect/6 - //if(lastElem !== document.body) { //faster but does gives false positive in Firefox - offsetLeft += window.pageXOffset || document.documentElement.scrollLeft; - offsetTop += window.pageYOffset || document.documentElement.scrollTop; - } - - return { - left: offsetLeft, - top: offsetTop - }; -}; - -WalkontableDom.prototype.getComputedStyle = function (elem) { - return elem.currentStyle || document.defaultView.getComputedStyle(elem); -}; - -WalkontableDom.prototype.outerWidth = function (elem) { - return elem.offsetWidth; -}; - -WalkontableDom.prototype.outerHeight = function (elem) { - if (this.hasCaptionProblem() && elem.firstChild && elem.firstChild.nodeName === 'CAPTION') { - //fixes problem with Firefox ignoring in TABLE.offsetHeight - //jQuery (1.10.1) still has this unsolved - //may be better to just switch to getBoundingClientRect - //http://bililite.com/blog/2009/03/27/finding-the-size-of-a-table/ - //http://lists.w3.org/Archives/Public/www-style/2009Oct/0089.html - //http://bugs.jquery.com/ticket/2196 - //http://lists.w3.org/Archives/Public/www-style/2009Oct/0140.html#start140 - return elem.offsetHeight + elem.firstChild.offsetHeight; - } - else { - return elem.offsetHeight; - } -}; - -(function () { - var hasCaptionProblem; - - function detectCaptionProblem() { - var TABLE = document.createElement('TABLE'); - TABLE.style.borderSpacing = 0; - TABLE.style.borderWidth = 0; - TABLE.style.padding = 0; - var TBODY = document.createElement('TBODY'); - TABLE.appendChild(TBODY); - TBODY.appendChild(document.createElement('TR')); - TBODY.firstChild.appendChild(document.createElement('TD')); - TBODY.firstChild.firstChild.innerHTML = '
    t
    t
    offsetWidth; -}; - -WalkontableViewport.prototype.getColumnHeaderHeight = function () { - if (isNaN(this.columnHeaderHeight)) { - var cellOffset = this.instance.wtDom.offset(this.instance.wtTable.TBODY) - , tableOffset = this.instance.wtTable.tableOffset; - this.columnHeaderHeight = cellOffset.top - tableOffset.top; - } - return this.columnHeaderHeight; -}; - -WalkontableViewport.prototype.getViewportHeight = function (proposedHeight) { - var containerHeight = this.getWorkspaceHeight(proposedHeight); - - if (containerHeight === Infinity) { - return containerHeight; - } - - var columnHeaderHeight = this.getColumnHeaderHeight(); - if (columnHeaderHeight > 0) { - return containerHeight - columnHeaderHeight; - } - else { - return containerHeight; - } -}; - -WalkontableViewport.prototype.getRowHeaderHeight = function () { - if (isNaN(this.rowHeaderWidth)) { - var TR = this.instance.wtTable.TBODY ? this.instance.wtTable.TBODY.firstChild : null; - if (TR) { - var TD = TR.firstChild; - this.rowHeaderWidth = 0; - while (TD && TD.nodeName === 'TH') { - this.rowHeaderWidth += this.instance.wtDom.outerWidth(TD); - TD = TD.nextSibling; - } - } - } - return this.rowHeaderWidth; -}; - -WalkontableViewport.prototype.getViewportWidth = function (proposedWidth) { - var containerWidth = this.getWorkspaceWidth(proposedWidth); - - if (containerWidth === Infinity) { - return containerWidth; - } - - var rowHeaderWidth = this.getRowHeaderHeight(); - if (rowHeaderWidth > 0) { - return containerWidth - rowHeaderWidth; - } - else { - return containerWidth; - } -}; - -WalkontableViewport.prototype.resetSettings = function () { - this.rowHeaderWidth = NaN; - this.columnHeaderHeight = NaN; -}; -function WalkontableWheel(instance) { - if (instance.isNativeScroll) { - return; - } - - //spreader === instance.wtTable.TABLE.parentNode - $(instance.wtTable.spreader).on('mousewheel', function (event, delta, deltaX, deltaY) { - if (!deltaX && !deltaY && delta) { //we are in IE8, see https://github.com/brandonaaron/jquery-mousewheel/issues/53 - deltaY = delta; - } - - if (!deltaX && !deltaY) { //this happens in IE8 test case - return; - } - - if (deltaY > 0 && instance.getSetting('offsetRow') === 0) { - return; //attempt to scroll up when it's already showing first row - } - else if (deltaY < 0 && instance.wtTable.isLastRowFullyVisible()) { - return; //attempt to scroll down when it's already showing last row - } - else if (deltaX < 0 && instance.getSetting('offsetColumn') === 0) { - return; //attempt to scroll left when it's already showing first column - } - else if (deltaX > 0 && instance.wtTable.isLastColumnFullyVisible()) { - return; //attempt to scroll right when it's already showing last column - } - - //now we are sure we really want to scroll - clearTimeout(instance.wheelTimeout); - instance.wheelTimeout = setTimeout(function () { //timeout is needed because with fast-wheel scrolling mousewheel event comes dozen times per second - if (deltaY) { - //ceil is needed because jquery-mousewheel reports fractional mousewheel deltas on touchpad scroll - //see http://stackoverflow.com/questions/5527601/normalizing-mousewheel-speed-across-browsers - if (instance.wtScrollbars.vertical.visible) { // if we see scrollbar - instance.scrollVertical(-Math.ceil(deltaY)).draw(); - } - } - else if (deltaX) { - if (instance.wtScrollbars.horizontal.visible) { // if we see scrollbar - instance.scrollHorizontal(Math.ceil(deltaX)).draw(); - } - } - }, 0); - - event.preventDefault(); - }); -} -/** - * Dragdealer JS v0.9.5 - patched by Walkontable at lines 66, 309-310, 339-340 - * http://code.ovidiu.ch/dragdealer-js - * - * Copyright (c) 2010, Ovidiu Chereches - * MIT License - * http://legal.ovidiu.ch/licenses/MIT - */ - -/* Cursor */ - -var Cursor = -{ - x: 0, y: 0, - init: function() - { - this.setEvent('mouse'); - this.setEvent('touch'); - }, - setEvent: function(type) - { - var moveHandler = document['on' + type + 'move'] || function(){}; - document['on' + type + 'move'] = function(e) - { - moveHandler(e); - Cursor.refresh(e); - } - }, - refresh: function(e) - { - if(!e) - { - e = window.event; - } - if(e.type == 'mousemove') - { - this.set(e); - } - else if(e.touches) - { - this.set(e.touches[0]); - } - }, - set: function(e) - { - if(e.pageX || e.pageY) - { - this.x = e.pageX; - this.y = e.pageY; - } - else if(e.clientX || e.clientY) - { - this.x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; - this.y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; - } - } -}; -Cursor.init(); - -/* Position */ - -var Position = -{ - get: function(obj) - { - var curtop = 0, curleft = 0; //Walkontable patch. Original (var curleft = curtop = 0;) created curtop in global scope - if(obj.offsetParent) - { - do - { - curleft += obj.offsetLeft; - curtop += obj.offsetTop; - } - while((obj = obj.offsetParent)); - } - return [curleft, curtop]; - } -}; - -/* Dragdealer */ - -var Dragdealer = function(wrapper, options) -{ - if(typeof(wrapper) == 'string') - { - wrapper = document.getElementById(wrapper); - } - if(!wrapper) - { - return; - } - var handle = wrapper.getElementsByTagName('div')[0]; - if(!handle || handle.className.search(/(^|\s)handle(\s|$)/) == -1) - { - return; - } - this.init(wrapper, handle, options || {}); - this.setup(); -}; -Dragdealer.prototype = -{ - init: function(wrapper, handle, options) - { - this.wrapper = wrapper; - this.handle = handle; - this.options = options; - - this.disabled = this.getOption('disabled', false); - this.horizontal = this.getOption('horizontal', true); - this.vertical = this.getOption('vertical', false); - this.slide = this.getOption('slide', true); - this.steps = this.getOption('steps', 0); - this.snap = this.getOption('snap', false); - this.loose = this.getOption('loose', false); - this.speed = this.getOption('speed', 10) / 100; - this.xPrecision = this.getOption('xPrecision', 0); - this.yPrecision = this.getOption('yPrecision', 0); - - this.callback = options.callback || null; - this.animationCallback = options.animationCallback || null; - - this.bounds = { - left: options.left || 0, right: -(options.right || 0), - top: options.top || 0, bottom: -(options.bottom || 0), - x0: 0, x1: 0, xRange: 0, - y0: 0, y1: 0, yRange: 0 - }; - this.value = { - prev: [-1, -1], - current: [options.x || 0, options.y || 0], - target: [options.x || 0, options.y || 0] - }; - this.offset = { - wrapper: [0, 0], - mouse: [0, 0], - prev: [-999999, -999999], - current: [0, 0], - target: [0, 0] - }; - this.change = [0, 0]; - - this.activity = false; - this.dragging = false; - this.tapping = false; - }, - getOption: function(name, defaultValue) - { - return this.options[name] !== undefined ? this.options[name] : defaultValue; - }, - setup: function() - { - this.setWrapperOffset(); - this.setBoundsPadding(); - this.setBounds(); - this.setSteps(); - - this.addListeners(); - }, - setWrapperOffset: function() - { - this.offset.wrapper = Position.get(this.wrapper); - }, - setBoundsPadding: function() - { - if(!this.bounds.left && !this.bounds.right) - { - this.bounds.left = Position.get(this.handle)[0] - this.offset.wrapper[0]; - this.bounds.right = -this.bounds.left; - } - if(!this.bounds.top && !this.bounds.bottom) - { - this.bounds.top = Position.get(this.handle)[1] - this.offset.wrapper[1]; - this.bounds.bottom = -this.bounds.top; - } - }, - setBounds: function() - { - this.bounds.x0 = this.bounds.left; - this.bounds.x1 = this.wrapper.offsetWidth + this.bounds.right; - this.bounds.xRange = (this.bounds.x1 - this.bounds.x0) - this.handle.offsetWidth; - - this.bounds.y0 = this.bounds.top; - this.bounds.y1 = this.wrapper.offsetHeight + this.bounds.bottom; - this.bounds.yRange = (this.bounds.y1 - this.bounds.y0) - this.handle.offsetHeight; - - this.bounds.xStep = 1 / (this.xPrecision || Math.max(this.wrapper.offsetWidth, this.handle.offsetWidth)); - this.bounds.yStep = 1 / (this.yPrecision || Math.max(this.wrapper.offsetHeight, this.handle.offsetHeight)); - }, - setSteps: function() - { - if(this.steps > 1) - { - this.stepRatios = []; - for(var i = 0; i <= this.steps - 1; i++) - { - this.stepRatios[i] = i / (this.steps - 1); - } - } - }, - addListeners: function() - { - var self = this; - - this.wrapper.onselectstart = function() - { - return false; - } - this.handle.onmousedown = this.handle.ontouchstart = function(e) - { - self.handleDownHandler(e); - }; - this.wrapper.onmousedown = this.wrapper.ontouchstart = function(e) - { - self.wrapperDownHandler(e); - }; - var mouseUpHandler = document.onmouseup || function(){}; - document.onmouseup = function(e) - { - mouseUpHandler(e); - self.documentUpHandler(e); - }; - var touchEndHandler = document.ontouchend || function(){}; - document.ontouchend = function(e) - { - touchEndHandler(e); - self.documentUpHandler(e); - }; - var resizeHandler = window.onresize || function(){}; - window.onresize = function(e) - { - resizeHandler(e); - self.documentResizeHandler(e); - }; - this.wrapper.onmousemove = function(e) - { - self.activity = true; - } - this.wrapper.onclick = function(e) - { - return !self.activity; - } - - this.interval = setInterval(function(){ self.animate() }, 25); - self.animate(false, true); - }, - handleDownHandler: function(e) - { - this.activity = false; - Cursor.refresh(e); - - this.preventDefaults(e, true); - this.startDrag(); - }, - wrapperDownHandler: function(e) - { - Cursor.refresh(e); - - this.preventDefaults(e, true); - this.startTap(); - }, - documentUpHandler: function(e) - { - this.stopDrag(); - this.stopTap(); - }, - documentResizeHandler: function(e) - { - this.setWrapperOffset(); - this.setBounds(); - - this.update(); - }, - enable: function() - { - this.disabled = false; - this.handle.className = this.handle.className.replace(/\s?disabled/g, ''); - }, - disable: function() - { - this.disabled = true; - this.handle.className += ' disabled'; - }, - setStep: function(x, y, snap) - { - this.setValue( - this.steps && x > 1 ? (x - 1) / (this.steps - 1) : 0, - this.steps && y > 1 ? (y - 1) / (this.steps - 1) : 0, - snap - ); - }, - setValue: function(x, y, snap) - { - this.setTargetValue([x, y || 0]); - if(snap) - { - this.groupCopy(this.value.current, this.value.target); - } - }, - startTap: function(target) - { - if(this.disabled) - { - return; - } - this.tapping = true; - - this.setWrapperOffset(); - this.setBounds(); - - if(target === undefined) - { - target = [ - Cursor.x - this.offset.wrapper[0] - (this.handle.offsetWidth / 2), - Cursor.y - this.offset.wrapper[1] - (this.handle.offsetHeight / 2) - ]; - } - this.setTargetOffset(target); - }, - stopTap: function() - { - if(this.disabled || !this.tapping) - { - return; - } - this.tapping = false; - - this.setTargetValue(this.value.current); - this.result(); - }, - startDrag: function() - { - if(this.disabled) - { - return; - } - - this.setWrapperOffset(); - this.setBounds(); - - this.offset.mouse = [ - Cursor.x - Position.get(this.handle)[0], - Cursor.y - Position.get(this.handle)[1] - ]; - - this.dragging = true; - }, - stopDrag: function() - { - if(this.disabled || !this.dragging) - { - return; - } - this.dragging = false; - - var target = this.groupClone(this.value.current); - if(this.slide) - { - var ratioChange = this.change; - target[0] += ratioChange[0] * 4; - target[1] += ratioChange[1] * 4; - } - this.setTargetValue(target); - this.result(); - }, - feedback: function() - { - var value = this.value.current; - if(this.snap && this.steps > 1) - { - value = this.getClosestSteps(value); - } - if(!this.groupCompare(value, this.value.prev)) - { - if(typeof(this.animationCallback) == 'function') - { - this.animationCallback(value[0], value[1]); - } - this.groupCopy(this.value.prev, value); - } - }, - result: function() - { - if(typeof(this.callback) == 'function') - { - this.callback(this.value.target[0], this.value.target[1]); - } - }, - animate: function(direct, first) - { - if(direct && !this.dragging) - { - return; - } - if(this.dragging) - { - var prevTarget = this.groupClone(this.value.target); - - var offset = [ - Cursor.x - this.offset.wrapper[0] - this.offset.mouse[0], - Cursor.y - this.offset.wrapper[1] - this.offset.mouse[1] - ]; - this.setTargetOffset(offset, this.loose); - - this.change = [ - this.value.target[0] - prevTarget[0], - this.value.target[1] - prevTarget[1] - ]; - } - if(this.dragging || first) - { - this.groupCopy(this.value.current, this.value.target); - } - if(this.dragging || this.glide() || first) - { - this.update(); - this.feedback(); - } - }, - glide: function() - { - var diff = [ - this.value.target[0] - this.value.current[0], - this.value.target[1] - this.value.current[1] - ]; - if(!diff[0] && !diff[1]) - { - return false; - } - if(Math.abs(diff[0]) > this.bounds.xStep || Math.abs(diff[1]) > this.bounds.yStep) - { - this.value.current[0] += diff[0] * this.speed; - this.value.current[1] += diff[1] * this.speed; - } - else - { - this.groupCopy(this.value.current, this.value.target); - } - return true; - }, - update: function() - { - if(!this.snap) - { - this.offset.current = this.getOffsetsByRatios(this.value.current); - } - else - { - this.offset.current = this.getOffsetsByRatios( - this.getClosestSteps(this.value.current) - ); - } - this.show(); - }, - show: function() - { - if(!this.groupCompare(this.offset.current, this.offset.prev)) - { - if(this.horizontal) - { - this.handle.style.left = String(this.offset.current[0]) + 'px'; - } - if(this.vertical) - { - this.handle.style.top = String(this.offset.current[1]) + 'px'; - } - this.groupCopy(this.offset.prev, this.offset.current); - } - }, - setTargetValue: function(value, loose) - { - var target = loose ? this.getLooseValue(value) : this.getProperValue(value); - - this.groupCopy(this.value.target, target); - this.offset.target = this.getOffsetsByRatios(target); - }, - setTargetOffset: function(offset, loose) - { - var value = this.getRatiosByOffsets(offset); - var target = loose ? this.getLooseValue(value) : this.getProperValue(value); - - this.groupCopy(this.value.target, target); - this.offset.target = this.getOffsetsByRatios(target); - }, - getLooseValue: function(value) - { - var proper = this.getProperValue(value); - return [ - proper[0] + ((value[0] - proper[0]) / 4), - proper[1] + ((value[1] - proper[1]) / 4) - ]; - }, - getProperValue: function(value) - { - var proper = this.groupClone(value); - - proper[0] = Math.max(proper[0], 0); - proper[1] = Math.max(proper[1], 0); - proper[0] = Math.min(proper[0], 1); - proper[1] = Math.min(proper[1], 1); - - if((!this.dragging && !this.tapping) || this.snap) - { - if(this.steps > 1) - { - proper = this.getClosestSteps(proper); - } - } - return proper; - }, - getRatiosByOffsets: function(group) - { - return [ - this.getRatioByOffset(group[0], this.bounds.xRange, this.bounds.x0), - this.getRatioByOffset(group[1], this.bounds.yRange, this.bounds.y0) - ]; - }, - getRatioByOffset: function(offset, range, padding) - { - return range ? (offset - padding) / range : 0; - }, - getOffsetsByRatios: function(group) - { - return [ - this.getOffsetByRatio(group[0], this.bounds.xRange, this.bounds.x0), - this.getOffsetByRatio(group[1], this.bounds.yRange, this.bounds.y0) - ]; - }, - getOffsetByRatio: function(ratio, range, padding) - { - return Math.round(ratio * range) + padding; - }, - getClosestSteps: function(group) - { - return [ - this.getClosestStep(group[0]), - this.getClosestStep(group[1]) - ]; - }, - getClosestStep: function(value) - { - var k = 0; - var min = 1; - for(var i = 0; i <= this.steps - 1; i++) - { - if(Math.abs(this.stepRatios[i] - value) < min) - { - min = Math.abs(this.stepRatios[i] - value); - k = i; - } - } - return this.stepRatios[k]; - }, - groupCompare: function(a, b) - { - return a[0] == b[0] && a[1] == b[1]; - }, - groupCopy: function(a, b) - { - a[0] = b[0]; - a[1] = b[1]; - }, - groupClone: function(a) - { - return [a[0], a[1]]; - }, - preventDefaults: function(e, selection) - { - if(!e) - { - e = window.event; - } - if(e.preventDefault) - { - e.preventDefault(); - } - e.returnValue = false; - - if(selection && document.selection) - { - document.selection.empty(); - } - }, - cancelEvent: function(e) - { - if(!e) - { - e = window.event; - } - if(e.stopPropagation) - { - e.stopPropagation(); - } - e.cancelBubble = true; - } -}; - -/** - * jQuery.browser shim that makes Walkontable working with jQuery 1.9+ - */ -if (!jQuery.browser) { - (function () { - var matched, browser; - - /* - * Copyright 2011, John Resig - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - */ - jQuery.uaMatch = function (ua) { - ua = ua.toLowerCase(); - - var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || - /(webkit)[ \/]([\w.]+)/.exec(ua) || - /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || - /(msie) ([\w.]+)/.exec(ua) || - ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || - []; - - return { - browser: match[ 1 ] || "", - version: match[ 2 ] || "0" - }; - }; - - matched = jQuery.uaMatch(navigator.userAgent); - browser = {}; - - if (matched.browser) { - browser[ matched.browser ] = true; - browser.version = matched.version; - } - - // Chrome is Webkit, but Webkit is also Safari. - if (browser.chrome) { - browser.webkit = true; - } - else if (browser.webkit) { - browser.safari = true; - } - - jQuery.browser = browser; - - })(); -} -/*! Copyright (c) 2013 Brandon Aaron (http://brandonaaron.net) - * Licensed under the MIT License (LICENSE.txt). - * - * Thanks to: http://adomas.org/javascript-mouse-wheel/ for some pointers. - * Thanks to: Mathias Bank(http://www.mathias-bank.de) for a scope bug fix. - * Thanks to: Seamus Leahy for adding deltaX and deltaY - * - * Version: 3.1.3 - * - * Requires: 1.2.2+ - */ - -(function (factory) { - if ( typeof define === 'function' && define.amd ) { - // AMD. Register as an anonymous module. - define(['jquery'], factory); - } else if (typeof exports === 'object') { - // Node/CommonJS style for Browserify - module.exports = factory; - } else { - // Browser globals - factory(jQuery); - } -}(function ($) { - - var toFix = ['wheel', 'mousewheel', 'DOMMouseScroll', 'MozMousePixelScroll']; - var toBind = 'onwheel' in document || document.documentMode >= 9 ? ['wheel'] : ['mousewheel', 'DomMouseScroll', 'MozMousePixelScroll']; - var lowestDelta, lowestDeltaXY; - - if ( $.event.fixHooks ) { - for ( var i = toFix.length; i; ) { - $.event.fixHooks[ toFix[--i] ] = $.event.mouseHooks; - } - } - - $.event.special.mousewheel = { - setup: function() { - if ( this.addEventListener ) { - for ( var i = toBind.length; i; ) { - this.addEventListener( toBind[--i], handler, false ); - } - } else { - this.onmousewheel = handler; - } - }, - - teardown: function() { - if ( this.removeEventListener ) { - for ( var i = toBind.length; i; ) { - this.removeEventListener( toBind[--i], handler, false ); - } - } else { - this.onmousewheel = null; - } - } - }; - - $.fn.extend({ - mousewheel: function(fn) { - return fn ? this.bind("mousewheel", fn) : this.trigger("mousewheel"); - }, - - unmousewheel: function(fn) { - return this.unbind("mousewheel", fn); - } - }); - - - function handler(event) { - var orgEvent = event || window.event, - args = [].slice.call(arguments, 1), - delta = 0, - deltaX = 0, - deltaY = 0, - absDelta = 0, - absDeltaXY = 0, - fn; - event = $.event.fix(orgEvent); - event.type = "mousewheel"; - - // Old school scrollwheel delta - if ( orgEvent.wheelDelta ) { delta = orgEvent.wheelDelta; } - if ( orgEvent.detail ) { delta = orgEvent.detail * -1; } - - // New school wheel delta (wheel event) - if ( orgEvent.deltaY ) { - deltaY = orgEvent.deltaY * -1; - delta = deltaY; - } - if ( orgEvent.deltaX ) { - deltaX = orgEvent.deltaX; - delta = deltaX * -1; - } - - // Webkit - if ( orgEvent.wheelDeltaY !== undefined ) { deltaY = orgEvent.wheelDeltaY; } - if ( orgEvent.wheelDeltaX !== undefined ) { deltaX = orgEvent.wheelDeltaX * -1; } - - // Look for lowest delta to normalize the delta values - absDelta = Math.abs(delta); - if ( !lowestDelta || absDelta < lowestDelta ) { lowestDelta = absDelta; } - absDeltaXY = Math.max(Math.abs(deltaY), Math.abs(deltaX)); - if ( !lowestDeltaXY || absDeltaXY < lowestDeltaXY ) { lowestDeltaXY = absDeltaXY; } - - // Get a whole value for the deltas - fn = delta > 0 ? 'floor' : 'ceil'; - delta = Math[fn](delta / lowestDelta); - deltaX = Math[fn](deltaX / lowestDeltaXY); - deltaY = Math[fn](deltaY / lowestDeltaXY); - - // Add event and delta to the front of the arguments - args.unshift(event, delta, deltaX, deltaY); - - return ($.event.dispatch || $.event.handle).apply(this, args); - } - -})); - -/** - * Array.filter() shim by Trevor Menagh (https://github.com/trevmex) with some modifications - */ - -if (!Array.prototype.filter) { - Array.prototype.filter = function (fun, thisp) { - "use strict"; - - if (typeof this === "undefined" || this === null) { - throw new TypeError(); - } - if (typeof fun !== "function") { - throw new TypeError(); - } - - thisp = thisp || this; - - if (isNodeList(thisp)) { - thisp = convertNodeListToArray(thisp); - } - - var len = thisp.length, - res = [], - i, - val; - - for (i = 0; i < len; i += 1) { - if (thisp.hasOwnProperty(i)) { - val = thisp[i]; // in case fun mutates this - if (fun.call(thisp, val, i, thisp)) { - res.push(val); - } - } - } - - return res; - - function isNodeList(object) { - return /NodeList/i.test(object.item); - } - - function convertNodeListToArray(nodeList) { - var array = []; - - for (var i = 0, len = nodeList.length; i < len; i++){ - array[i] = nodeList[i] - } - - return array; - } - }; -} - -})(jQuery, window, Handsontable); \ No newline at end of file diff --git a/bower_components/handsontable/dist_wc/README.md b/bower_components/handsontable/dist_wc/README.md deleted file mode 100644 index db4284ba..00000000 --- a/bower_components/handsontable/dist_wc/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Handsontable Web Components distribution - -As a future alternative to jQuery, we are experimenting with Web Components version of Handsontable (see demo page for more details). - -To use the current experimental version, just load the Toolkitchen Toolkit library and import `x-handsontable.html` component. - -```html - - - - - - -``` \ No newline at end of file diff --git a/bower_components/handsontable/dist_wc/x-handsontable.html b/bower_components/handsontable/dist_wc/x-handsontable.html deleted file mode 100644 index 9ab2de06..00000000 --- a/bower_components/handsontable/dist_wc/x-handsontable.html +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/bower_components/handsontable/dist_wc/x-handsontable/jquery-2.min.js b/bower_components/handsontable/dist_wc/x-handsontable/jquery-2.min.js deleted file mode 100644 index 2be209dd..00000000 --- a/bower_components/handsontable/dist_wc/x-handsontable/jquery-2.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/*! jQuery v2.0.3 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license -//@ sourceMappingURL=jquery-2.0.3.min.map -*/ -(function(e,undefined){var t,n,r=typeof undefined,i=e.location,o=e.document,s=o.documentElement,a=e.jQuery,u=e.$,l={},c=[],p="2.0.3",f=c.concat,h=c.push,d=c.slice,g=c.indexOf,m=l.toString,y=l.hasOwnProperty,v=p.trim,x=function(e,n){return new x.fn.init(e,n,t)},b=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,w=/\S+/g,T=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,k=/^-ms-/,N=/-([\da-z])/gi,E=function(e,t){return t.toUpperCase()},S=function(){o.removeEventListener("DOMContentLoaded",S,!1),e.removeEventListener("load",S,!1),x.ready()};x.fn=x.prototype={jquery:p,constructor:x,init:function(e,t,n){var r,i;if(!e)return this;if("string"==typeof e){if(r="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:T.exec(e),!r||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof x?t[0]:t,x.merge(this,x.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:o,!0)),C.test(r[1])&&x.isPlainObject(t))for(r in t)x.isFunction(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return i=o.getElementById(r[2]),i&&i.parentNode&&(this.length=1,this[0]=i),this.context=o,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?n.ready(e):(e.selector!==undefined&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return d.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,t,n,r,i,o,s=arguments[0]||{},a=1,u=arguments.length,l=!1;for("boolean"==typeof s&&(l=s,s=arguments[1]||{},a=2),"object"==typeof s||x.isFunction(s)||(s={}),u===a&&(s=this,--a);u>a;a++)if(null!=(e=arguments[a]))for(t in e)n=s[t],r=e[t],s!==r&&(l&&r&&(x.isPlainObject(r)||(i=x.isArray(r)))?(i?(i=!1,o=n&&x.isArray(n)?n:[]):o=n&&x.isPlainObject(n)?n:{},s[t]=x.extend(l,o,r)):r!==undefined&&(s[t]=r));return s},x.extend({expando:"jQuery"+(p+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=a),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){(e===!0?--x.readyWait:x.isReady)||(x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(o,[x]),x.fn.trigger&&x(o).trigger("ready").off("ready")))},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray,isWindow:function(e){return null!=e&&e===e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[m.call(e)]||"object":typeof e},isPlainObject:function(e){if("object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!y.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(t){return!1}return!0},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||o;var r=C.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:JSON.parse,parseXML:function(e){var t,n;if(!e||"string"!=typeof e)return null;try{n=new DOMParser,t=n.parseFromString(e,"text/xml")}catch(r){t=undefined}return(!t||t.getElementsByTagName("parsererror").length)&&x.error("Invalid XML: "+e),t},noop:function(){},globalEval:function(e){var t,n=eval;e=x.trim(e),e&&(1===e.indexOf("use strict")?(t=o.createElement("script"),t.text=e,o.head.appendChild(t).parentNode.removeChild(t)):n(e))},camelCase:function(e){return e.replace(k,"ms-").replace(N,E)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,s=j(e);if(n){if(s){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(s){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:function(e){return null==e?"":v.call(e)},makeArray:function(e,t){var n=t||[];return null!=e&&(j(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:g.call(t,e,n)},merge:function(e,t){var n=t.length,r=e.length,i=0;if("number"==typeof n)for(;n>i;i++)e[r++]=t[i];else while(t[i]!==undefined)e[r++]=t[i++];return e.length=r,e},grep:function(e,t,n){var r,i=[],o=0,s=e.length;for(n=!!n;s>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,s=j(e),a=[];if(s)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(a[a.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(a[a.length]=r);return f.apply([],a)},guid:1,proxy:function(e,t){var n,r,i;return"string"==typeof t&&(n=e[t],t=e,e=n),x.isFunction(e)?(r=d.call(arguments,2),i=function(){return e.apply(t||this,r.concat(d.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):undefined},access:function(e,t,n,r,i,o,s){var a=0,u=e.length,l=null==n;if("object"===x.type(n)){i=!0;for(a in n)x.access(e,t,a,n[a],!0,o,s)}else if(r!==undefined&&(i=!0,x.isFunction(r)||(s=!0),l&&(s?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(x(e),n)})),t))for(;u>a;a++)t(e[a],n,s?r:r.call(e[a],a,t(e[a],n)));return i?e:l?t.call(e):u?t(e[0],n):o},now:Date.now,swap:function(e,t,n,r){var i,o,s={};for(o in t)s[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=s[o];return i}}),x.ready.promise=function(t){return n||(n=x.Deferred(),"complete"===o.readyState?setTimeout(x.ready):(o.addEventListener("DOMContentLoaded",S,!1),e.addEventListener("load",S,!1))),n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){l["[object "+t+"]"]=t.toLowerCase()});function j(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}t=x(o),function(e,undefined){var t,n,r,i,o,s,a,u,l,c,p,f,h,d,g,m,y,v="sizzle"+-new Date,b=e.document,w=0,T=0,C=st(),k=st(),N=st(),E=!1,S=function(e,t){return e===t?(E=!0,0):0},j=typeof undefined,D=1<<31,A={}.hasOwnProperty,L=[],q=L.pop,H=L.push,O=L.push,F=L.slice,P=L.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},R="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",W="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",$=W.replace("w","w#"),B="\\["+M+"*("+W+")"+M+"*(?:([*^$|!~]?=)"+M+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+$+")|)|)"+M+"*\\]",I=":("+W+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+B.replace(3,8)+")*)|.*)\\)|)",z=RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),_=RegExp("^"+M+"*,"+M+"*"),X=RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=RegExp(M+"*[+~]"),Y=RegExp("="+M+"*([^\\]'\"]*)"+M+"*\\]","g"),V=RegExp(I),G=RegExp("^"+$+"$"),J={ID:RegExp("^#("+W+")"),CLASS:RegExp("^\\.("+W+")"),TAG:RegExp("^("+W.replace("w","w*")+")"),ATTR:RegExp("^"+B),PSEUDO:RegExp("^"+I),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:RegExp("^(?:"+R+")$","i"),needsContext:RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Q=/^[^{]+\{\s*\[native \w/,K=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,Z=/^(?:input|select|textarea|button)$/i,et=/^h\d$/i,tt=/'|\\/g,nt=RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),rt=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(55296|r>>10,56320|1023&r)};try{O.apply(L=F.call(b.childNodes),b.childNodes),L[b.childNodes.length].nodeType}catch(it){O={apply:L.length?function(e,t){H.apply(e,F.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function ot(e,t,r,i){var o,s,a,u,l,f,g,m,x,w;if((t?t.ownerDocument||t:b)!==p&&c(t),t=t||p,r=r||[],!e||"string"!=typeof e)return r;if(1!==(u=t.nodeType)&&9!==u)return[];if(h&&!i){if(o=K.exec(e))if(a=o[1]){if(9===u){if(s=t.getElementById(a),!s||!s.parentNode)return r;if(s.id===a)return r.push(s),r}else if(t.ownerDocument&&(s=t.ownerDocument.getElementById(a))&&y(t,s)&&s.id===a)return r.push(s),r}else{if(o[2])return O.apply(r,t.getElementsByTagName(e)),r;if((a=o[3])&&n.getElementsByClassName&&t.getElementsByClassName)return O.apply(r,t.getElementsByClassName(a)),r}if(n.qsa&&(!d||!d.test(e))){if(m=g=v,x=t,w=9===u&&e,1===u&&"object"!==t.nodeName.toLowerCase()){f=gt(e),(g=t.getAttribute("id"))?m=g.replace(tt,"\\$&"):t.setAttribute("id",m),m="[id='"+m+"'] ",l=f.length;while(l--)f[l]=m+mt(f[l]);x=U.test(e)&&t.parentNode||t,w=f.join(",")}if(w)try{return O.apply(r,x.querySelectorAll(w)),r}catch(T){}finally{g||t.removeAttribute("id")}}}return kt(e.replace(z,"$1"),t,r,i)}function st(){var e=[];function t(n,r){return e.push(n+=" ")>i.cacheLength&&delete t[e.shift()],t[n]=r}return t}function at(e){return e[v]=!0,e}function ut(e){var t=p.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function lt(e,t){var n=e.split("|"),r=e.length;while(r--)i.attrHandle[n[r]]=t}function ct(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function pt(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function ft(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function ht(e){return at(function(t){return t=+t,at(function(n,r){var i,o=e([],n.length,t),s=o.length;while(s--)n[i=o[s]]&&(n[i]=!(r[i]=n[i]))})})}s=ot.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},n=ot.support={},c=ot.setDocument=function(e){var t=e?e.ownerDocument||e:b,r=t.defaultView;return t!==p&&9===t.nodeType&&t.documentElement?(p=t,f=t.documentElement,h=!s(t),r&&r.attachEvent&&r!==r.top&&r.attachEvent("onbeforeunload",function(){c()}),n.attributes=ut(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ut(function(e){return e.appendChild(t.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=ut(function(e){return e.innerHTML="
    ",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),n.getById=ut(function(e){return f.appendChild(e).id=v,!t.getElementsByName||!t.getElementsByName(v).length}),n.getById?(i.find.ID=function(e,t){if(typeof t.getElementById!==j&&h){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},i.filter.ID=function(e){var t=e.replace(nt,rt);return function(e){return e.getAttribute("id")===t}}):(delete i.find.ID,i.filter.ID=function(e){var t=e.replace(nt,rt);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),i.find.TAG=n.getElementsByTagName?function(e,t){return typeof t.getElementsByTagName!==j?t.getElementsByTagName(e):undefined}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},i.find.CLASS=n.getElementsByClassName&&function(e,t){return typeof t.getElementsByClassName!==j&&h?t.getElementsByClassName(e):undefined},g=[],d=[],(n.qsa=Q.test(t.querySelectorAll))&&(ut(function(e){e.innerHTML="",e.querySelectorAll("[selected]").length||d.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll(":checked").length||d.push(":checked")}),ut(function(e){var n=t.createElement("input");n.setAttribute("type","hidden"),e.appendChild(n).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&d.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||d.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),d.push(",.*:")})),(n.matchesSelector=Q.test(m=f.webkitMatchesSelector||f.mozMatchesSelector||f.oMatchesSelector||f.msMatchesSelector))&&ut(function(e){n.disconnectedMatch=m.call(e,"div"),m.call(e,"[s!='']:x"),g.push("!=",I)}),d=d.length&&RegExp(d.join("|")),g=g.length&&RegExp(g.join("|")),y=Q.test(f.contains)||f.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},S=f.compareDocumentPosition?function(e,r){if(e===r)return E=!0,0;var i=r.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(r);return i?1&i||!n.sortDetached&&r.compareDocumentPosition(e)===i?e===t||y(b,e)?-1:r===t||y(b,r)?1:l?P.call(l,e)-P.call(l,r):0:4&i?-1:1:e.compareDocumentPosition?-1:1}:function(e,n){var r,i=0,o=e.parentNode,s=n.parentNode,a=[e],u=[n];if(e===n)return E=!0,0;if(!o||!s)return e===t?-1:n===t?1:o?-1:s?1:l?P.call(l,e)-P.call(l,n):0;if(o===s)return ct(e,n);r=e;while(r=r.parentNode)a.unshift(r);r=n;while(r=r.parentNode)u.unshift(r);while(a[i]===u[i])i++;return i?ct(a[i],u[i]):a[i]===b?-1:u[i]===b?1:0},t):p},ot.matches=function(e,t){return ot(e,null,null,t)},ot.matchesSelector=function(e,t){if((e.ownerDocument||e)!==p&&c(e),t=t.replace(Y,"='$1']"),!(!n.matchesSelector||!h||g&&g.test(t)||d&&d.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(i){}return ot(t,p,null,[e]).length>0},ot.contains=function(e,t){return(e.ownerDocument||e)!==p&&c(e),y(e,t)},ot.attr=function(e,t){(e.ownerDocument||e)!==p&&c(e);var r=i.attrHandle[t.toLowerCase()],o=r&&A.call(i.attrHandle,t.toLowerCase())?r(e,t,!h):undefined;return o===undefined?n.attributes||!h?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null:o},ot.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},ot.uniqueSort=function(e){var t,r=[],i=0,o=0;if(E=!n.detectDuplicates,l=!n.sortStable&&e.slice(0),e.sort(S),E){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return e},o=ot.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=o(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=o(t);return n},i=ot.selectors={cacheLength:50,createPseudo:at,match:J,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(nt,rt),e[3]=(e[4]||e[5]||"").replace(nt,rt),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||ot.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&ot.error(e[0]),e},PSEUDO:function(e){var t,n=!e[5]&&e[2];return J.CHILD.test(e[0])?null:(e[3]&&e[4]!==undefined?e[2]=e[4]:n&&V.test(n)&&(t=gt(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(nt,rt).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=C[e+" "];return t||(t=RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&C(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=ot.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),s="last"!==e.slice(-4),a="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,p,f,h,d,g=o!==s?"nextSibling":"previousSibling",m=t.parentNode,y=a&&t.nodeName.toLowerCase(),x=!u&&!a;if(m){if(o){while(g){p=t;while(p=p[g])if(a?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;d=g="only"===e&&!d&&"nextSibling"}return!0}if(d=[s?m.firstChild:m.lastChild],s&&x){c=m[v]||(m[v]={}),l=c[e]||[],h=l[0]===w&&l[1],f=l[0]===w&&l[2],p=h&&m.childNodes[h];while(p=++h&&p&&p[g]||(f=h=0)||d.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[w,h,f];break}}else if(x&&(l=(t[v]||(t[v]={}))[e])&&l[0]===w)f=l[1];else while(p=++h&&p&&p[g]||(f=h=0)||d.pop())if((a?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(x&&((p[v]||(p[v]={}))[e]=[w,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=i.pseudos[e]||i.setFilters[e.toLowerCase()]||ot.error("unsupported pseudo: "+e);return r[v]?r(t):r.length>1?(n=[e,e,"",t],i.setFilters.hasOwnProperty(e.toLowerCase())?at(function(e,n){var i,o=r(e,t),s=o.length;while(s--)i=P.call(e,o[s]),e[i]=!(n[i]=o[s])}):function(e){return r(e,0,n)}):r}},pseudos:{not:at(function(e){var t=[],n=[],r=a(e.replace(z,"$1"));return r[v]?at(function(e,t,n,i){var o,s=r(e,null,i,[]),a=e.length;while(a--)(o=s[a])&&(e[a]=!(t[a]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:at(function(e){return function(t){return ot(e,t).length>0}}),contains:at(function(e){return function(t){return(t.textContent||t.innerText||o(t)).indexOf(e)>-1}}),lang:at(function(e){return G.test(e||"")||ot.error("unsupported lang: "+e),e=e.replace(nt,rt).toLowerCase(),function(t){var n;do if(n=h?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===f},focus:function(e){return e===p.activeElement&&(!p.hasFocus||p.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!i.pseudos.empty(e)},header:function(e){return et.test(e.nodeName)},input:function(e){return Z.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:ht(function(){return[0]}),last:ht(function(e,t){return[t-1]}),eq:ht(function(e,t,n){return[0>n?n+t:n]}),even:ht(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:ht(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:ht(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:ht(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}},i.pseudos.nth=i.pseudos.eq;for(t in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})i.pseudos[t]=pt(t);for(t in{submit:!0,reset:!0})i.pseudos[t]=ft(t);function dt(){}dt.prototype=i.filters=i.pseudos,i.setFilters=new dt;function gt(e,t){var n,r,o,s,a,u,l,c=k[e+" "];if(c)return t?0:c.slice(0);a=e,u=[],l=i.preFilter;while(a){(!n||(r=_.exec(a)))&&(r&&(a=a.slice(r[0].length)||a),u.push(o=[])),n=!1,(r=X.exec(a))&&(n=r.shift(),o.push({value:n,type:r[0].replace(z," ")}),a=a.slice(n.length));for(s in i.filter)!(r=J[s].exec(a))||l[s]&&!(r=l[s](r))||(n=r.shift(),o.push({value:n,type:s,matches:r}),a=a.slice(n.length));if(!n)break}return t?a.length:a?ot.error(e):k(e,u).slice(0)}function mt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function yt(e,t,n){var i=t.dir,o=n&&"parentNode"===i,s=T++;return t.first?function(t,n,r){while(t=t[i])if(1===t.nodeType||o)return e(t,n,r)}:function(t,n,a){var u,l,c,p=w+" "+s;if(a){while(t=t[i])if((1===t.nodeType||o)&&e(t,n,a))return!0}else while(t=t[i])if(1===t.nodeType||o)if(c=t[v]||(t[v]={}),(l=c[i])&&l[0]===p){if((u=l[1])===!0||u===r)return u===!0}else if(l=c[i]=[p],l[1]=e(t,n,a)||r,l[1]===!0)return!0}}function vt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function xt(e,t,n,r,i){var o,s=[],a=0,u=e.length,l=null!=t;for(;u>a;a++)(o=e[a])&&(!n||n(o,r,i))&&(s.push(o),l&&t.push(a));return s}function bt(e,t,n,r,i,o){return r&&!r[v]&&(r=bt(r)),i&&!i[v]&&(i=bt(i,o)),at(function(o,s,a,u){var l,c,p,f=[],h=[],d=s.length,g=o||Ct(t||"*",a.nodeType?[a]:a,[]),m=!e||!o&&t?g:xt(g,f,e,a,u),y=n?i||(o?e:d||r)?[]:s:m;if(n&&n(m,y,a,u),r){l=xt(y,h),r(l,[],a,u),c=l.length;while(c--)(p=l[c])&&(y[h[c]]=!(m[h[c]]=p))}if(o){if(i||e){if(i){l=[],c=y.length;while(c--)(p=y[c])&&l.push(m[c]=p);i(null,y=[],l,u)}c=y.length;while(c--)(p=y[c])&&(l=i?P.call(o,p):f[c])>-1&&(o[l]=!(s[l]=p))}}else y=xt(y===s?y.splice(d,y.length):y),i?i(null,s,y,u):O.apply(s,y)})}function wt(e){var t,n,r,o=e.length,s=i.relative[e[0].type],a=s||i.relative[" "],l=s?1:0,c=yt(function(e){return e===t},a,!0),p=yt(function(e){return P.call(t,e)>-1},a,!0),f=[function(e,n,r){return!s&&(r||n!==u)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;o>l;l++)if(n=i.relative[e[l].type])f=[yt(vt(f),n)];else{if(n=i.filter[e[l].type].apply(null,e[l].matches),n[v]){for(r=++l;o>r;r++)if(i.relative[e[r].type])break;return bt(l>1&&vt(f),l>1&&mt(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(z,"$1"),n,r>l&&wt(e.slice(l,r)),o>r&&wt(e=e.slice(r)),o>r&&mt(e))}f.push(n)}return vt(f)}function Tt(e,t){var n=0,o=t.length>0,s=e.length>0,a=function(a,l,c,f,h){var d,g,m,y=[],v=0,x="0",b=a&&[],T=null!=h,C=u,k=a||s&&i.find.TAG("*",h&&l.parentNode||l),N=w+=null==C?1:Math.random()||.1;for(T&&(u=l!==p&&l,r=n);null!=(d=k[x]);x++){if(s&&d){g=0;while(m=e[g++])if(m(d,l,c)){f.push(d);break}T&&(w=N,r=++n)}o&&((d=!m&&d)&&v--,a&&b.push(d))}if(v+=x,o&&x!==v){g=0;while(m=t[g++])m(b,y,l,c);if(a){if(v>0)while(x--)b[x]||y[x]||(y[x]=q.call(f));y=xt(y)}O.apply(f,y),T&&!a&&y.length>0&&v+t.length>1&&ot.uniqueSort(f)}return T&&(w=N,u=C),b};return o?at(a):a}a=ot.compile=function(e,t){var n,r=[],i=[],o=N[e+" "];if(!o){t||(t=gt(e)),n=t.length;while(n--)o=wt(t[n]),o[v]?r.push(o):i.push(o);o=N(e,Tt(i,r))}return o};function Ct(e,t,n){var r=0,i=t.length;for(;i>r;r++)ot(e,t[r],n);return n}function kt(e,t,r,o){var s,u,l,c,p,f=gt(e);if(!o&&1===f.length){if(u=f[0]=f[0].slice(0),u.length>2&&"ID"===(l=u[0]).type&&n.getById&&9===t.nodeType&&h&&i.relative[u[1].type]){if(t=(i.find.ID(l.matches[0].replace(nt,rt),t)||[])[0],!t)return r;e=e.slice(u.shift().value.length)}s=J.needsContext.test(e)?0:u.length;while(s--){if(l=u[s],i.relative[c=l.type])break;if((p=i.find[c])&&(o=p(l.matches[0].replace(nt,rt),U.test(u[0].type)&&t.parentNode||t))){if(u.splice(s,1),e=o.length&&mt(u),!e)return O.apply(r,o),r;break}}}return a(e,f)(o,t,!h,r,U.test(e)),r}n.sortStable=v.split("").sort(S).join("")===v,n.detectDuplicates=E,c(),n.sortDetached=ut(function(e){return 1&e.compareDocumentPosition(p.createElement("div"))}),ut(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||lt("type|href|height|width",function(e,t,n){return n?undefined:e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ut(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||lt("value",function(e,t,n){return n||"input"!==e.nodeName.toLowerCase()?undefined:e.defaultValue}),ut(function(e){return null==e.getAttribute("disabled")})||lt(R,function(e,t,n){var r;return n?undefined:(r=e.getAttributeNode(t))&&r.specified?r.value:e[t]===!0?t.toLowerCase():null}),x.find=ot,x.expr=ot.selectors,x.expr[":"]=x.expr.pseudos,x.unique=ot.uniqueSort,x.text=ot.getText,x.isXMLDoc=ot.isXML,x.contains=ot.contains}(e);var D={};function A(e){var t=D[e]={};return x.each(e.match(w)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?D[e]||A(e):x.extend({},e);var t,n,r,i,o,s,a=[],u=!e.once&&[],l=function(p){for(t=e.memory&&p,n=!0,s=i||0,i=0,o=a.length,r=!0;a&&o>s;s++)if(a[s].apply(p[0],p[1])===!1&&e.stopOnFalse){t=!1;break}r=!1,a&&(u?u.length&&l(u.shift()):t?a=[]:c.disable())},c={add:function(){if(a){var n=a.length;(function s(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&c.has(n)||a.push(n):n&&n.length&&"string"!==r&&s(n)})})(arguments),r?o=a.length:t&&(i=n,l(t))}return this},remove:function(){return a&&x.each(arguments,function(e,t){var n;while((n=x.inArray(t,a,n))>-1)a.splice(n,1),r&&(o>=n&&o--,s>=n&&s--)}),this},has:function(e){return e?x.inArray(e,a)>-1:!(!a||!a.length)},empty:function(){return a=[],o=0,this},disable:function(){return a=u=t=undefined,this},disabled:function(){return!a},lock:function(){return u=undefined,t||c.disable(),this},locked:function(){return!u},fireWith:function(e,t){return!a||n&&!u||(t=t||[],t=[e,t.slice?t.slice():t],r?u.push(t):l(t)),this},fire:function(){return c.fireWith(this,arguments),this},fired:function(){return!!n}};return c},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var s=o[0],a=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=a&&a.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[s+"With"](this===r?n.promise():this,a?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o){var s=o[2],a=o[3];r[o[1]]=s.add,a&&s.add(function(){n=a},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=s.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=d.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),s=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?d.call(arguments):r,n===a?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},a,u,l;if(r>1)for(a=Array(r),u=Array(r),l=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(s(t,l,n)).fail(o.reject).progress(s(t,u,a)):--i;return i||o.resolveWith(l,n),o.promise()}}),x.support=function(t){var n=o.createElement("input"),r=o.createDocumentFragment(),i=o.createElement("div"),s=o.createElement("select"),a=s.appendChild(o.createElement("option"));return n.type?(n.type="checkbox",t.checkOn=""!==n.value,t.optSelected=a.selected,t.reliableMarginRight=!0,t.boxSizingReliable=!0,t.pixelPosition=!1,n.checked=!0,t.noCloneChecked=n.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!a.disabled,n=o.createElement("input"),n.value="t",n.type="radio",t.radioValue="t"===n.value,n.setAttribute("checked","t"),n.setAttribute("name","t"),r.appendChild(n),t.checkClone=r.cloneNode(!0).cloneNode(!0).lastChild.checked,t.focusinBubbles="onfocusin"in e,i.style.backgroundClip="content-box",i.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===i.style.backgroundClip,x(function(){var n,r,s="padding:0;margin:0;border:0;display:block;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box",a=o.getElementsByTagName("body")[0];a&&(n=o.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",a.appendChild(n).appendChild(i),i.innerHTML="",i.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%",x.swap(a,null!=a.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===i.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(i,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(i,null)||{width:"4px"}).width,r=i.appendChild(o.createElement("div")),r.style.cssText=i.style.cssText=s,r.style.marginRight=r.style.width="0",i.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),a.removeChild(n))}),t):t}({});var L,q,H=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,O=/([A-Z])/g;function F(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=x.expando+Math.random()}F.uid=1,F.accepts=function(e){return e.nodeType?1===e.nodeType||9===e.nodeType:!0},F.prototype={key:function(e){if(!F.accepts(e))return 0;var t={},n=e[this.expando];if(!n){n=F.uid++;try{t[this.expando]={value:n},Object.defineProperties(e,t)}catch(r){t[this.expando]=n,x.extend(e,t)}}return this.cache[n]||(this.cache[n]={}),n},set:function(e,t,n){var r,i=this.key(e),o=this.cache[i];if("string"==typeof t)o[t]=n;else if(x.isEmptyObject(o))x.extend(this.cache[i],t);else for(r in t)o[r]=t[r];return o},get:function(e,t){var n=this.cache[this.key(e)];return t===undefined?n:n[t]},access:function(e,t,n){var r;return t===undefined||t&&"string"==typeof t&&n===undefined?(r=this.get(e,t),r!==undefined?r:this.get(e,x.camelCase(t))):(this.set(e,t,n),n!==undefined?n:t)},remove:function(e,t){var n,r,i,o=this.key(e),s=this.cache[o];if(t===undefined)this.cache[o]={};else{x.isArray(t)?r=t.concat(t.map(x.camelCase)):(i=x.camelCase(t),t in s?r=[t,i]:(r=i,r=r in s?[r]:r.match(w)||[])),n=r.length;while(n--)delete s[r[n]]}},hasData:function(e){return!x.isEmptyObject(this.cache[e[this.expando]]||{})},discard:function(e){e[this.expando]&&delete this.cache[e[this.expando]]}},L=new F,q=new F,x.extend({acceptData:F.accepts,hasData:function(e){return L.hasData(e)||q.hasData(e)},data:function(e,t,n){return L.access(e,t,n)},removeData:function(e,t){L.remove(e,t)},_data:function(e,t,n){return q.access(e,t,n)},_removeData:function(e,t){q.remove(e,t)}}),x.fn.extend({data:function(e,t){var n,r,i=this[0],o=0,s=null;if(e===undefined){if(this.length&&(s=L.get(i),1===i.nodeType&&!q.get(i,"hasDataAttrs"))){for(n=i.attributes;n.length>o;o++)r=n[o].name,0===r.indexOf("data-")&&(r=x.camelCase(r.slice(5)),P(i,r,s[r]));q.set(i,"hasDataAttrs",!0)}return s}return"object"==typeof e?this.each(function(){L.set(this,e)}):x.access(this,function(t){var n,r=x.camelCase(e);if(i&&t===undefined){if(n=L.get(i,e),n!==undefined)return n;if(n=L.get(i,r),n!==undefined)return n;if(n=P(i,r,undefined),n!==undefined)return n}else this.each(function(){var n=L.get(this,r);L.set(this,r,t),-1!==e.indexOf("-")&&n!==undefined&&L.set(this,e,t)})},null,t,arguments.length>1,null,!0)},removeData:function(e){return this.each(function(){L.remove(this,e)})}});function P(e,t,n){var r;if(n===undefined&&1===e.nodeType)if(r="data-"+t.replace(O,"-$1").toLowerCase(),n=e.getAttribute(r),"string"==typeof n){try{n="true"===n?!0:"false"===n?!1:"null"===n?null:+n+""===n?+n:H.test(n)?JSON.parse(n):n}catch(i){}L.set(e,t,n)}else n=undefined;return n}x.extend({queue:function(e,t,n){var r;return e?(t=(t||"fx")+"queue",r=q.get(e,t),n&&(!r||x.isArray(n)?r=q.access(e,t,x.makeArray(n)):r.push(n)),r||[]):undefined},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),s=function(){x.dequeue(e,t) -};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,s,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return q.get(e,n)||q.access(e,n,{empty:x.Callbacks("once memory").add(function(){q.remove(e,[t+"queue",n])})})}}),x.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),n>arguments.length?x.queue(this[0],e):t===undefined?this:this.each(function(){var n=x.queue(this,e,t);x._queueHooks(this,e),"fx"===e&&"inprogress"!==n[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=x.Deferred(),o=this,s=this.length,a=function(){--r||i.resolveWith(o,[o])};"string"!=typeof e&&(t=e,e=undefined),e=e||"fx";while(s--)n=q.get(o[s],e+"queueHooks"),n&&n.empty&&(r++,n.empty.add(a));return a(),i.promise(t)}});var R,M,W=/[\t\r\n\f]/g,$=/\r/g,B=/^(?:input|select|textarea|button)$/i;x.fn.extend({attr:function(e,t){return x.access(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[x.propFix[e]||e]})},addClass:function(e){var t,n,r,i,o,s=0,a=this.length,u="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];a>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(W," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,s=0,a=this.length,u=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];a>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(W," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var t,i=0,o=x(this),s=e.match(w)||[];while(t=s[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else(n===r||"boolean"===n)&&(this.className&&q.set(this,"__className__",this.className),this.className=this.className||e===!1?"":q.get(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(W," ").indexOf(t)>=0)return!0;return!1},val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=x.isFunction(e),this.each(function(n){var i;1===this.nodeType&&(i=r?e.call(this,n,x(this).val()):e,null==i?i="":"number"==typeof i?i+="":x.isArray(i)&&(i=x.map(i,function(e){return null==e?"":e+""})),t=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],t&&"set"in t&&t.set(this,i,"value")!==undefined||(this.value=i))});if(i)return t=x.valHooks[i.type]||x.valHooks[i.nodeName.toLowerCase()],t&&"get"in t&&(n=t.get(i,"value"))!==undefined?n:(n=i.value,"string"==typeof n?n.replace($,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=e.attributes.value;return!t||t.specified?e.value:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,s=o?null:[],a=o?i+1:r.length,u=0>i?a:o?i:0;for(;a>u;u++)if(n=r[u],!(!n.selected&&u!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;s.push(t)}return s},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),s=i.length;while(s--)r=i[s],(r.selected=x.inArray(x(r).val(),o)>=0)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,t,n){var i,o,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===r?x.prop(e,t,n):(1===s&&x.isXMLDoc(e)||(t=t.toLowerCase(),i=x.attrHooks[t]||(x.expr.match.bool.test(t)?M:R)),n===undefined?i&&"get"in i&&null!==(o=i.get(e,t))?o:(o=x.find.attr(e,t),null==o?undefined:o):null!==n?i&&"set"in i&&(o=i.set(e,n,t))!==undefined?o:(e.setAttribute(t,n+""),n):(x.removeAttr(e,t),undefined))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(w);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.bool.test(n)&&(e[r]=!1),e.removeAttribute(n)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,t,n){var r,i,o,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return o=1!==s||!x.isXMLDoc(e),o&&(t=x.propFix[t]||t,i=x.propHooks[t]),n!==undefined?i&&"set"in i&&(r=i.set(e,n,t))!==undefined?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){return e.hasAttribute("tabindex")||B.test(e.nodeName)||e.href?e.tabIndex:-1}}}}),M={set:function(e,t,n){return t===!1?x.removeAttr(e,n):e.setAttribute(n,n),n}},x.each(x.expr.match.bool.source.match(/\w+/g),function(e,t){var n=x.expr.attrHandle[t]||x.find.attr;x.expr.attrHandle[t]=function(e,t,r){var i=x.expr.attrHandle[t],o=r?undefined:(x.expr.attrHandle[t]=undefined)!=n(e,t,r)?t.toLowerCase():null;return x.expr.attrHandle[t]=i,o}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,t){return x.isArray(t)?e.checked=x.inArray(x(e).val(),t)>=0:undefined}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var I=/^key/,z=/^(?:mouse|contextmenu)|click/,_=/^(?:focusinfocus|focusoutblur)$/,X=/^([^.]*)(?:\.(.+)|)$/;function U(){return!0}function Y(){return!1}function V(){try{return o.activeElement}catch(e){}}x.event={global:{},add:function(e,t,n,i,o){var s,a,u,l,c,p,f,h,d,g,m,y=q.get(e);if(y){n.handler&&(s=n,n=s.handler,o=s.selector),n.guid||(n.guid=x.guid++),(l=y.events)||(l=y.events={}),(a=y.handle)||(a=y.handle=function(e){return typeof x===r||e&&x.event.triggered===e.type?undefined:x.event.dispatch.apply(a.elem,arguments)},a.elem=e),t=(t||"").match(w)||[""],c=t.length;while(c--)u=X.exec(t[c])||[],d=m=u[1],g=(u[2]||"").split(".").sort(),d&&(f=x.event.special[d]||{},d=(o?f.delegateType:f.bindType)||d,f=x.event.special[d]||{},p=x.extend({type:d,origType:m,data:i,handler:n,guid:n.guid,selector:o,needsContext:o&&x.expr.match.needsContext.test(o),namespace:g.join(".")},s),(h=l[d])||(h=l[d]=[],h.delegateCount=0,f.setup&&f.setup.call(e,i,g,a)!==!1||e.addEventListener&&e.addEventListener(d,a,!1)),f.add&&(f.add.call(e,p),p.handler.guid||(p.handler.guid=n.guid)),o?h.splice(h.delegateCount++,0,p):h.push(p),x.event.global[d]=!0);e=null}},remove:function(e,t,n,r,i){var o,s,a,u,l,c,p,f,h,d,g,m=q.hasData(e)&&q.get(e);if(m&&(u=m.events)){t=(t||"").match(w)||[""],l=t.length;while(l--)if(a=X.exec(t[l])||[],h=g=a[1],d=(a[2]||"").split(".").sort(),h){p=x.event.special[h]||{},h=(r?p.delegateType:p.bindType)||h,f=u[h]||[],a=a[2]&&RegExp("(^|\\.)"+d.join("\\.(?:.*\\.|)")+"(\\.|$)"),s=o=f.length;while(o--)c=f[o],!i&&g!==c.origType||n&&n.guid!==c.guid||a&&!a.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(f.splice(o,1),c.selector&&f.delegateCount--,p.remove&&p.remove.call(e,c));s&&!f.length&&(p.teardown&&p.teardown.call(e,d,m.handle)!==!1||x.removeEvent(e,h,m.handle),delete u[h])}else for(h in u)x.event.remove(e,h+t[l],n,r,!0);x.isEmptyObject(u)&&(delete m.handle,q.remove(e,"events"))}},trigger:function(t,n,r,i){var s,a,u,l,c,p,f,h=[r||o],d=y.call(t,"type")?t.type:t,g=y.call(t,"namespace")?t.namespace.split("."):[];if(a=u=r=r||o,3!==r.nodeType&&8!==r.nodeType&&!_.test(d+x.event.triggered)&&(d.indexOf(".")>=0&&(g=d.split("."),d=g.shift(),g.sort()),c=0>d.indexOf(":")&&"on"+d,t=t[x.expando]?t:new x.Event(d,"object"==typeof t&&t),t.isTrigger=i?2:3,t.namespace=g.join("."),t.namespace_re=t.namespace?RegExp("(^|\\.)"+g.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=undefined,t.target||(t.target=r),n=null==n?[t]:x.makeArray(n,[t]),f=x.event.special[d]||{},i||!f.trigger||f.trigger.apply(r,n)!==!1)){if(!i&&!f.noBubble&&!x.isWindow(r)){for(l=f.delegateType||d,_.test(l+d)||(a=a.parentNode);a;a=a.parentNode)h.push(a),u=a;u===(r.ownerDocument||o)&&h.push(u.defaultView||u.parentWindow||e)}s=0;while((a=h[s++])&&!t.isPropagationStopped())t.type=s>1?l:f.bindType||d,p=(q.get(a,"events")||{})[t.type]&&q.get(a,"handle"),p&&p.apply(a,n),p=c&&a[c],p&&x.acceptData(a)&&p.apply&&p.apply(a,n)===!1&&t.preventDefault();return t.type=d,i||t.isDefaultPrevented()||f._default&&f._default.apply(h.pop(),n)!==!1||!x.acceptData(r)||c&&x.isFunction(r[d])&&!x.isWindow(r)&&(u=r[c],u&&(r[c]=null),x.event.triggered=d,r[d](),x.event.triggered=undefined,u&&(r[c]=u)),t.result}},dispatch:function(e){e=x.event.fix(e);var t,n,r,i,o,s=[],a=d.call(arguments),u=(q.get(this,"events")||{})[e.type]||[],l=x.event.special[e.type]||{};if(a[0]=e,e.delegateTarget=this,!l.preDispatch||l.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this,e,u),t=0;while((i=s[t++])&&!e.isPropagationStopped()){e.currentTarget=i.elem,n=0;while((o=i.handlers[n++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(o.namespace))&&(e.handleObj=o,e.data=o.data,r=((x.event.special[o.origType]||{}).handle||o.handler).apply(i.elem,a),r!==undefined&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return l.postDispatch&&l.postDispatch.call(this,e),e.result}},handlers:function(e,t){var n,r,i,o,s=[],a=t.delegateCount,u=e.target;if(a&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!==this;u=u.parentNode||this)if(u.disabled!==!0||"click"!==e.type){for(r=[],n=0;a>n;n++)o=t[n],i=o.selector+" ",r[i]===undefined&&(r[i]=o.needsContext?x(i,this).index(u)>=0:x.find(i,this,null,[u]).length),r[i]&&r.push(o);r.length&&s.push({elem:u,handlers:r})}return t.length>a&&s.push({elem:this,handlers:t.slice(a)}),s},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,t){var n,r,i,s=t.button;return null==e.pageX&&null!=t.clientX&&(n=e.target.ownerDocument||o,r=n.documentElement,i=n.body,e.pageX=t.clientX+(r&&r.scrollLeft||i&&i.scrollLeft||0)-(r&&r.clientLeft||i&&i.clientLeft||0),e.pageY=t.clientY+(r&&r.scrollTop||i&&i.scrollTop||0)-(r&&r.clientTop||i&&i.clientTop||0)),e.which||s===undefined||(e.which=1&s?1:2&s?3:4&s?2:0),e}},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,s=e,a=this.fixHooks[i];a||(this.fixHooks[i]=a=z.test(i)?this.mouseHooks:I.test(i)?this.keyHooks:{}),r=a.props?this.props.concat(a.props):this.props,e=new x.Event(s),t=r.length;while(t--)n=r[t],e[n]=s[n];return e.target||(e.target=o),3===e.target.nodeType&&(e.target=e.target.parentNode),a.filter?a.filter(e,s):e},special:{load:{noBubble:!0},focus:{trigger:function(){return this!==V()&&this.focus?(this.focus(),!1):undefined},delegateType:"focusin"},blur:{trigger:function(){return this===V()&&this.blur?(this.blur(),!1):undefined},delegateType:"focusout"},click:{trigger:function(){return"checkbox"===this.type&&this.click&&x.nodeName(this,"input")?(this.click(),!1):undefined},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==undefined&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)},x.Event=function(e,t){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.getPreventDefault&&e.getPreventDefault()?U:Y):this.type=e,t&&x.extend(this,t),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,undefined):new x.Event(e,t)},x.Event.prototype={isDefaultPrevented:Y,isPropagationStopped:Y,isImmediatePropagationStopped:Y,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=U,e&&e.preventDefault&&e.preventDefault()},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=U,e&&e.stopPropagation&&e.stopPropagation()},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=U,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0===n++&&o.addEventListener(e,r,!0)},teardown:function(){0===--n&&o.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,t,n,r,i){var o,s;if("object"==typeof e){"string"!=typeof t&&(n=n||t,t=undefined);for(s in e)this.on(s,t,n,e[s],i);return this}if(null==n&&null==r?(r=t,n=t=undefined):null==r&&("string"==typeof t?(r=n,n=undefined):(r=n,n=t,t=undefined)),r===!1)r=Y;else if(!r)return this;return 1===i&&(o=r,r=function(e){return x().off(e),o.apply(this,arguments)},r.guid=o.guid||(o.guid=x.guid++)),this.each(function(){x.event.add(this,e,r,n,t)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,x(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return(t===!1||"function"==typeof t)&&(n=t,t=undefined),n===!1&&(n=Y),this.each(function(){x.event.remove(this,e,n,t)})},trigger:function(e,t){return this.each(function(){x.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];return n?x.event.trigger(e,t,n,!0):undefined}});var G=/^.[^:#\[\.,]*$/,J=/^(?:parents|prev(?:Until|All))/,Q=x.expr.match.needsContext,K={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(x(e).filter(function(){for(t=0;i>t;t++)if(x.contains(r[t],this))return!0}));for(t=0;i>t;t++)x.find(e,r[t],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},has:function(e){var t=x(e,this),n=t.length;return this.filter(function(){var e=0;for(;n>e;e++)if(x.contains(this,t[e]))return!0})},not:function(e){return this.pushStack(et(this,e||[],!0))},filter:function(e){return this.pushStack(et(this,e||[],!1))},is:function(e){return!!et(this,"string"==typeof e&&Q.test(e)?x(e):e||[],!1).length},closest:function(e,t){var n,r=0,i=this.length,o=[],s=Q.test(e)||"string"!=typeof e?x(e,t||this.context):0;for(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(s?s.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?g.call(x(e),this[0]):g.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function Z(e,t){while((e=e[t])&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return Z(e,"nextSibling")},prev:function(e){return Z(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling")},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return e.contentDocument||x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(K[e]||x.unique(i),J.test(e)&&i.reverse()),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,t,n){var r=[],i=n!==undefined;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&x(e).is(n))break;r.push(e)}return r},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function et(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(G.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return g.call(t,e)>=0!==n})}var tt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,nt=/<([\w:]+)/,rt=/<|&#?\w+;/,it=/<(?:script|style|link)/i,ot=/^(?:checkbox|radio)$/i,st=/checked\s*(?:[^=]|=\s*.checked.)/i,at=/^$|\/(?:java|ecma)script/i,ut=/^true\/(.*)/,lt=/^\s*\s*$/g,ct={option:[1,""],thead:[1,"
    ","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};ct.optgroup=ct.option,ct.tbody=ct.tfoot=ct.colgroup=ct.caption=ct.thead,ct.th=ct.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===undefined?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||o).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=pt(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=pt(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(mt(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&dt(mt(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++)1===e.nodeType&&(x.cleanData(mt(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.access(this,function(e){var t=this[0]||{},n=0,r=this.length;if(e===undefined&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!it.test(e)&&!ct[(nt.exec(e)||["",""])[1].toLowerCase()]){e=e.replace(tt,"<$1>");try{for(;r>n;n++)t=this[n]||{},1===t.nodeType&&(x.cleanData(mt(t,!1)),t.innerHTML=e);t=0}catch(i){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(r&&r.parentNode!==i&&(r=this.nextSibling),x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=f.apply([],e);var r,i,o,s,a,u,l=0,c=this.length,p=this,h=c-1,d=e[0],g=x.isFunction(d);if(g||!(1>=c||"string"!=typeof d||x.support.checkClone)&&st.test(d))return this.each(function(r){var i=p.eq(r);g&&(e[0]=d.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(r=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),i=r.firstChild,1===r.childNodes.length&&(r=i),i)){for(o=x.map(mt(r,"script"),ft),s=o.length;c>l;l++)a=r,l!==h&&(a=x.clone(a,!0,!0),s&&x.merge(o,mt(a,"script"))),t.call(this[l],a,l);if(s)for(u=o[o.length-1].ownerDocument,x.map(o,ht),l=0;s>l;l++)a=o[l],at.test(a.type||"")&&!q.access(a,"globalEval")&&x.contains(u,a)&&(a.src?x._evalUrl(a.src):x.globalEval(a.textContent.replace(lt,"")))}return this}}),x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){x.fn[e]=function(e){var n,r=[],i=x(e),o=i.length-1,s=0;for(;o>=s;s++)n=s===o?this:this.clone(!0),x(i[s])[t](n),h.apply(r,n.get());return this.pushStack(r)}}),x.extend({clone:function(e,t,n){var r,i,o,s,a=e.cloneNode(!0),u=x.contains(e.ownerDocument,e);if(!(x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(s=mt(a),o=mt(e),r=0,i=o.length;i>r;r++)yt(o[r],s[r]);if(t)if(n)for(o=o||mt(e),s=s||mt(a),r=0,i=o.length;i>r;r++)gt(o[r],s[r]);else gt(e,a);return s=mt(a,"script"),s.length>0&&dt(s,!u&&mt(e,"script")),a},buildFragment:function(e,t,n,r){var i,o,s,a,u,l,c=0,p=e.length,f=t.createDocumentFragment(),h=[];for(;p>c;c++)if(i=e[c],i||0===i)if("object"===x.type(i))x.merge(h,i.nodeType?[i]:i);else if(rt.test(i)){o=o||f.appendChild(t.createElement("div")),s=(nt.exec(i)||["",""])[1].toLowerCase(),a=ct[s]||ct._default,o.innerHTML=a[1]+i.replace(tt,"<$1>")+a[2],l=a[0];while(l--)o=o.lastChild;x.merge(h,o.childNodes),o=f.firstChild,o.textContent=""}else h.push(t.createTextNode(i));f.textContent="",c=0;while(i=h[c++])if((!r||-1===x.inArray(i,r))&&(u=x.contains(i.ownerDocument,i),o=mt(f.appendChild(i),"script"),u&&dt(o),n)){l=0;while(i=o[l++])at.test(i.type||"")&&n.push(i)}return f},cleanData:function(e){var t,n,r,i,o,s,a=x.event.special,u=0;for(;(n=e[u])!==undefined;u++){if(F.accepts(n)&&(o=n[q.expando],o&&(t=q.cache[o]))){if(r=Object.keys(t.events||{}),r.length)for(s=0;(i=r[s])!==undefined;s++)a[i]?x.event.remove(n,i):x.removeEvent(n,i,t.handle);q.cache[o]&&delete q.cache[o]}delete L.cache[n[L.expando]]}},_evalUrl:function(e){return x.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})}});function pt(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function ft(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function ht(e){var t=ut.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function dt(e,t){var n=e.length,r=0;for(;n>r;r++)q.set(e[r],"globalEval",!t||q.get(t[r],"globalEval"))}function gt(e,t){var n,r,i,o,s,a,u,l;if(1===t.nodeType){if(q.hasData(e)&&(o=q.access(e),s=q.set(t,o),l=o.events)){delete s.handle,s.events={};for(i in l)for(n=0,r=l[i].length;r>n;n++)x.event.add(t,i,l[i][n])}L.hasData(e)&&(a=L.access(e),u=x.extend({},a),L.set(t,u))}}function mt(e,t){var n=e.getElementsByTagName?e.getElementsByTagName(t||"*"):e.querySelectorAll?e.querySelectorAll(t||"*"):[];return t===undefined||t&&x.nodeName(e,t)?x.merge([e],n):n}function yt(e,t){var n=t.nodeName.toLowerCase();"input"===n&&ot.test(e.type)?t.checked=e.checked:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}x.fn.extend({wrapAll:function(e){var t;return x.isFunction(e)?this.each(function(t){x(this).wrapAll(e.call(this,t))}):(this[0]&&(t=x(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this)},wrapInner:function(e){return x.isFunction(e)?this.each(function(t){x(this).wrapInner(e.call(this,t))}):this.each(function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=x.isFunction(e);return this.each(function(n){x(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){x.nodeName(this,"body")||x(this).replaceWith(this.childNodes)}).end()}});var vt,xt,bt=/^(none|table(?!-c[ea]).+)/,wt=/^margin/,Tt=RegExp("^("+b+")(.*)$","i"),Ct=RegExp("^("+b+")(?!px)[a-z%]+$","i"),kt=RegExp("^([+-])=("+b+")","i"),Nt={BODY:"block"},Et={position:"absolute",visibility:"hidden",display:"block"},St={letterSpacing:0,fontWeight:400},jt=["Top","Right","Bottom","Left"],Dt=["Webkit","O","Moz","ms"];function At(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=Dt.length;while(i--)if(t=Dt[i]+n,t in e)return t;return r}function Lt(e,t){return e=t||e,"none"===x.css(e,"display")||!x.contains(e.ownerDocument,e)}function qt(t){return e.getComputedStyle(t,null)}function Ht(e,t){var n,r,i,o=[],s=0,a=e.length;for(;a>s;s++)r=e[s],r.style&&(o[s]=q.get(r,"olddisplay"),n=r.style.display,t?(o[s]||"none"!==n||(r.style.display=""),""===r.style.display&&Lt(r)&&(o[s]=q.access(r,"olddisplay",Rt(r.nodeName)))):o[s]||(i=Lt(r),(n&&"none"!==n||!i)&&q.set(r,"olddisplay",i?n:x.css(r,"display"))));for(s=0;a>s;s++)r=e[s],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[s]||"":"none"));return e}x.fn.extend({css:function(e,t){return x.access(this,function(e,t,n){var r,i,o={},s=0;if(x.isArray(t)){for(r=qt(e),i=t.length;i>s;s++)o[t[s]]=x.css(e,t[s],!1,r);return o}return n!==undefined?x.style(e,t,n):x.css(e,t)},e,t,arguments.length>1)},show:function(){return Ht(this,!0)},hide:function(){return Ht(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){Lt(this)?x(this).show():x(this).hide()})}}),x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=vt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,s,a=x.camelCase(t),u=e.style;return t=x.cssProps[a]||(x.cssProps[a]=At(u,a)),s=x.cssHooks[t]||x.cssHooks[a],n===undefined?s&&"get"in s&&(i=s.get(e,!1,r))!==undefined?i:u[t]:(o=typeof n,"string"===o&&(i=kt.exec(n))&&(n=(i[1]+1)*i[2]+parseFloat(x.css(e,t)),o="number"),null==n||"number"===o&&isNaN(n)||("number"!==o||x.cssNumber[a]||(n+="px"),x.support.clearCloneStyle||""!==n||0!==t.indexOf("background")||(u[t]="inherit"),s&&"set"in s&&(n=s.set(e,n,r))===undefined||(u[t]=n)),undefined)}},css:function(e,t,n,r){var i,o,s,a=x.camelCase(t);return t=x.cssProps[a]||(x.cssProps[a]=At(e.style,a)),s=x.cssHooks[t]||x.cssHooks[a],s&&"get"in s&&(i=s.get(e,!0,n)),i===undefined&&(i=vt(e,t,r)),"normal"===i&&t in St&&(i=St[t]),""===n||n?(o=parseFloat(i),n===!0||x.isNumeric(o)?o||0:i):i}}),vt=function(e,t,n){var r,i,o,s=n||qt(e),a=s?s.getPropertyValue(t)||s[t]:undefined,u=e.style;return s&&(""!==a||x.contains(e.ownerDocument,e)||(a=x.style(e,t)),Ct.test(a)&&wt.test(t)&&(r=u.width,i=u.minWidth,o=u.maxWidth,u.minWidth=u.maxWidth=u.width=a,a=s.width,u.width=r,u.minWidth=i,u.maxWidth=o)),a};function Ot(e,t,n){var r=Tt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function Ft(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,s=0;for(;4>o;o+=2)"margin"===n&&(s+=x.css(e,n+jt[o],!0,i)),r?("content"===n&&(s-=x.css(e,"padding"+jt[o],!0,i)),"margin"!==n&&(s-=x.css(e,"border"+jt[o]+"Width",!0,i))):(s+=x.css(e,"padding"+jt[o],!0,i),"padding"!==n&&(s+=x.css(e,"border"+jt[o]+"Width",!0,i)));return s}function Pt(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=qt(e),s=x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=vt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Ct.test(i))return i;r=s&&(x.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+Ft(e,t,n||(s?"border":"content"),r,o)+"px"}function Rt(e){var t=o,n=Nt[e];return n||(n=Mt(e,t),"none"!==n&&n||(xt=(xt||x(" +

    Handsontable is a minimalist Excel-like data grid + editor + for HTML & JavaScript

    - - +
    + This is Handsontable , a + release published on Nov 27, 2014. -
    a minimalistic Excel-like data grid editor - for HTML, JavaScript & jQuery -
    +

    The most prominent changes are:

    -
    - This page has been moved to - http://handsontable.com/. Please update your bookmarks. -
    +
      +
    • the removal of jQuery from dependencies and the filename,
    • +
    • (but we still keep the jQuery plugin API for backward compatibility), +
    • +
    • dropped support for IE 8 and 9 (if it bothers you, please email us),
    • +
    • more efficient row rendering algorithm.
    • +
    -
    - This is a new version . It introduces more plugin hooks (for Events and Callbacks) and more flexible Options.
    It should fully compatible - with 0.8 API. If not, please take a look at the migration guide in Wiki or write us a note on GitHub Issues.
    For details on what's changed, - see - Changelog. -
    -
    + Check out the full release + notes. If you experience some rough edges, please report an + issue or temporarily stick to + version 0.10.5 . + -
    -
    -
    -
    -

    Basic usage

    -
    -
    -
    - - - +
    +
    -
    -
    -
    - - - -
    - - -
    -

    Examples and how-to's

    - - - -
    -

    Cell types

    - + + + +
    -
    -

    Editing

    -
    - - -
    - -

    Source & Docs

    - - - -
    - -

    Discuss

    - - - -
    - -

    Authors

    - -

    - © 2013 Marcin Warpechowski / Nextgen.pl. Code and documentation licensed under The MIT License -

    - + - - - \ No newline at end of file + diff --git a/bower_components/handsontable/lib/bootstrap-typeahead.js b/bower_components/handsontable/lib/bootstrap-typeahead.js deleted file mode 100644 index ce8cb608..00000000 --- a/bower_components/handsontable/lib/bootstrap-typeahead.js +++ /dev/null @@ -1,335 +0,0 @@ -/* ============================================================= - * bootstrap-typeahead.js v2.3.1 - * http://twitter.github.com/bootstrap/javascript.html#typeahead - * ============================================================= - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ - - -!function($){ - - "use strict"; // jshint ;_; - - - /* TYPEAHEAD PUBLIC CLASS DEFINITION - * ================================= */ - - var Typeahead = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, $.fn.typeahead.defaults, options) - this.matcher = this.options.matcher || this.matcher - this.sorter = this.options.sorter || this.sorter - this.highlighter = this.options.highlighter || this.highlighter - this.updater = this.options.updater || this.updater - this.source = this.options.source - this.$menu = $(this.options.menu) - this.shown = false - this.listen() - } - - Typeahead.prototype = { - - constructor: Typeahead - - , select: function () { - var val = this.$menu.find('.active').attr('data-value') - this.$element - .val(this.updater(val)) - .change() - return this.hide() - } - - , updater: function (item) { - return item - } - - , show: function () { - var pos = $.extend({}, this.$element.position(), { - height: this.$element[0].offsetHeight - }) - - this.$menu - .insertAfter(this.$element) - .css({ - top: pos.top + pos.height - , left: pos.left - }) - .show() - - this.shown = true - return this - } - - , hide: function () { - this.$menu.hide() - this.shown = false - return this - } - - , lookup: function (event) { - var items - - this.query = this.$element.val() - - if (!this.query || this.query.length < this.options.minLength) { - return this.shown ? this.hide() : this - } - - items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source - - return items ? this.process(items) : this - } - - , process: function (items) { - var that = this - - items = $.grep(items, function (item) { - return that.matcher(item) - }) - - items = this.sorter(items) - - if (!items.length) { - return this.shown ? this.hide() : this - } - - return this.render(items.slice(0, this.options.items)).show() - } - - , matcher: function (item) { - return ~item.toLowerCase().indexOf(this.query.toLowerCase()) - } - - , sorter: function (items) { - var beginswith = [] - , caseSensitive = [] - , caseInsensitive = [] - , item - - while (item = items.shift()) { - if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item) - else if (~item.indexOf(this.query)) caseSensitive.push(item) - else caseInsensitive.push(item) - } - - return beginswith.concat(caseSensitive, caseInsensitive) - } - - , highlighter: function (item) { - var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&') - return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) { - return '' + match + '' - }) - } - - , render: function (items) { - var that = this - - items = $(items).map(function (i, item) { - i = $(that.options.item).attr('data-value', item) - i.find('a').html(that.highlighter(item)) - return i[0] - }) - - items.first().addClass('active') - this.$menu.html(items) - return this - } - - , next: function (event) { - var active = this.$menu.find('.active').removeClass('active') - , next = active.next() - - if (!next.length) { - next = $(this.$menu.find('li')[0]) - } - - next.addClass('active') - } - - , prev: function (event) { - var active = this.$menu.find('.active').removeClass('active') - , prev = active.prev() - - if (!prev.length) { - prev = this.$menu.find('li').last() - } - - prev.addClass('active') - } - - , listen: function () { - this.$element - .on('focus', $.proxy(this.focus, this)) - .on('blur', $.proxy(this.blur, this)) - .on('keypress', $.proxy(this.keypress, this)) - .on('keyup', $.proxy(this.keyup, this)) - - if (this.eventSupported('keydown')) { - this.$element.on('keydown', $.proxy(this.keydown, this)) - } - - this.$menu - .on('click', $.proxy(this.click, this)) - .on('mouseenter', 'li', $.proxy(this.mouseenter, this)) - .on('mouseleave', 'li', $.proxy(this.mouseleave, this)) - } - - , eventSupported: function(eventName) { - var isSupported = eventName in this.$element - if (!isSupported) { - this.$element.setAttribute(eventName, 'return;') - isSupported = typeof this.$element[eventName] === 'function' - } - return isSupported - } - - , move: function (e) { - if (!this.shown) return - - switch(e.keyCode) { - case 9: // tab - case 13: // enter - case 27: // escape - e.preventDefault() - break - - case 38: // up arrow - e.preventDefault() - this.prev() - break - - case 40: // down arrow - e.preventDefault() - this.next() - break - } - - e.stopPropagation() - } - - , keydown: function (e) { - this.suppressKeyPressRepeat = ~$.inArray(e.keyCode, [40,38,9,13,27]) - this.move(e) - } - - , keypress: function (e) { - if (this.suppressKeyPressRepeat) return - this.move(e) - } - - , keyup: function (e) { - switch(e.keyCode) { - case 40: // down arrow - case 38: // up arrow - case 16: // shift - case 17: // ctrl - case 18: // alt - break - - case 9: // tab - case 13: // enter - if (!this.shown) return - this.select() - break - - case 27: // escape - if (!this.shown) return - this.hide() - break - - default: - this.lookup() - } - - e.stopPropagation() - e.preventDefault() - } - - , focus: function (e) { - this.focused = true - } - - , blur: function (e) { - this.focused = false - if (!this.mousedover && this.shown) this.hide() - } - - , click: function (e) { - e.stopPropagation() - e.preventDefault() - this.select() - this.$element.focus() - } - - , mouseenter: function (e) { - this.mousedover = true - this.$menu.find('.active').removeClass('active') - $(e.currentTarget).addClass('active') - } - - , mouseleave: function (e) { - this.mousedover = false - if (!this.focused && this.shown) this.hide() - } - - } - - - /* TYPEAHEAD PLUGIN DEFINITION - * =========================== */ - - var old = $.fn.typeahead - - $.fn.typeahead = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('typeahead') - , options = typeof option == 'object' && option - if (!data) $this.data('typeahead', (data = new Typeahead(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - $.fn.typeahead.defaults = { - source: [] - , items: 8 - , menu: '' - , item: '
  • ' - , minLength: 1 - } - - $.fn.typeahead.Constructor = Typeahead - - - /* TYPEAHEAD NO CONFLICT - * =================== */ - - $.fn.typeahead.noConflict = function () { - $.fn.typeahead = old - return this - } - - - /* TYPEAHEAD DATA-API - * ================== */ - - $(document).on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) { - var $this = $(this) - if ($this.data('typeahead')) return - $this.typeahead($this.data()) - }) - -}(window.jQuery); \ No newline at end of file diff --git a/bower_components/handsontable/lib/jQuery-contextMenu/jquery.contextMenu.css b/bower_components/handsontable/lib/jQuery-contextMenu/jquery.contextMenu.css deleted file mode 100644 index e8e32b07..00000000 --- a/bower_components/handsontable/lib/jQuery-contextMenu/jquery.contextMenu.css +++ /dev/null @@ -1,142 +0,0 @@ -/*! - * jQuery contextMenu - Plugin for simple contextMenu handling - * - * Version: 1.6.5 - * - * Authors: Rodney Rehm, Addy Osmani (patches for FF) - * Web: http://medialize.github.com/jQuery-contextMenu/ - * - * Licensed under - * MIT License http://www.opensource.org/licenses/mit-license - * GPL v3 http://opensource.org/licenses/GPL-3.0 - * - */ - -.context-menu-list { - margin:0; - padding:0; - - min-width: 120px; - max-width: 250px; - display: inline-block; - position: absolute; - list-style-type: none; - - border: 1px solid #DDD; - background: #EEE; - - -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); - -moz-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); - -ms-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); - -o-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); - - font-family: Verdana, Arial, Helvetica, sans-serif; - font-size: 11px; -} - -.context-menu-item { - padding: 2px 2px 2px 24px; - background-color: #EEE; - position: relative; - -webkit-user-select: none; - -moz-user-select: -moz-none; - -ms-user-select: none; - user-select: none; -} - -.context-menu-separator { - padding-bottom:0; - border-bottom: 1px solid #DDD; -} - -.context-menu-item > label > input, -.context-menu-item > label > textarea { - -webkit-user-select: text; - -moz-user-select: text; - -ms-user-select: text; - user-select: text; -} - -.context-menu-item.hover { - cursor: pointer; - background-color: #39F; -} - -.context-menu-item.disabled { - color: #666; -} - -.context-menu-input.hover, -.context-menu-item.disabled.hover { - cursor: default; - background-color: #EEE; -} - -.context-menu-submenu:after { - content: ">"; - color: #666; - position: absolute; - top: 0; - right: 3px; - z-index: 1; -} - -/* icons - #protip: - In case you want to use sprites for icons (which I would suggest you do) have a look at - http://css-tricks.com/13224-pseudo-spriting/ to get an idea of how to implement - .context-menu-item.icon:before {} - */ -.context-menu-item.icon { min-height: 18px; background-repeat: no-repeat; background-position: 4px 2px; } -.context-menu-item.icon-edit { background-image: url(images/page_white_edit.png); } -.context-menu-item.icon-cut { background-image: url(images/cut.png); } -.context-menu-item.icon-copy { background-image: url(images/page_white_copy.png); } -.context-menu-item.icon-paste { background-image: url(images/page_white_paste.png); } -.context-menu-item.icon-delete { background-image: url(images/page_white_delete.png); } -.context-menu-item.icon-add { background-image: url(images/page_white_add.png); } -.context-menu-item.icon-quit { background-image: url(images/door.png); } - -/* vertically align inside labels */ -.context-menu-input > label > * { vertical-align: top; } - -/* position checkboxes and radios as icons */ -.context-menu-input > label > input[type="checkbox"], -.context-menu-input > label > input[type="radio"] { - margin-left: -17px; -} -.context-menu-input > label > span { - margin-left: 5px; -} - -.context-menu-input > label, -.context-menu-input > label > input[type="text"], -.context-menu-input > label > textarea, -.context-menu-input > label > select { - display: block; - width: 100%; - - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - -ms-box-sizing: border-box; - -o-box-sizing: border-box; - box-sizing: border-box; -} - -.context-menu-input > label > textarea { - height: 100px; -} -.context-menu-item > .context-menu-list { - display: none; - /* re-positioned by js */ - right: -5px; - top: 5px; -} - -.context-menu-item.hover > .context-menu-list { - display: block; -} - -.context-menu-accesskey { - text-decoration: underline; -} diff --git a/bower_components/handsontable/lib/jQuery-contextMenu/jquery.contextMenu.js b/bower_components/handsontable/lib/jQuery-contextMenu/jquery.contextMenu.js deleted file mode 100644 index f591d983..00000000 --- a/bower_components/handsontable/lib/jQuery-contextMenu/jquery.contextMenu.js +++ /dev/null @@ -1,1686 +0,0 @@ -/*! - * jQuery contextMenu - Plugin for simple contextMenu handling - * - * Version: 1.6.5 - * - * Authors: Rodney Rehm, Addy Osmani (patches for FF) - * Web: http://medialize.github.com/jQuery-contextMenu/ - * - * Licensed under - * MIT License http://www.opensource.org/licenses/mit-license - * GPL v3 http://opensource.org/licenses/GPL-3.0 - * - */ - -(function($, undefined){ - - // TODO: - - // ARIA stuff: menuitem, menuitemcheckbox und menuitemradio - // create structure if $.support[htmlCommand || htmlMenuitem] and !opt.disableNative - -// determine html5 compatibility -$.support.htmlMenuitem = ('HTMLMenuItemElement' in window); -$.support.htmlCommand = ('HTMLCommandElement' in window); -$.support.eventSelectstart = ("onselectstart" in document.documentElement); -/* // should the need arise, test for css user-select -$.support.cssUserSelect = (function(){ - var t = false, - e = document.createElement('div'); - - $.each('Moz|Webkit|Khtml|O|ms|Icab|'.split('|'), function(i, prefix) { - var propCC = prefix + (prefix ? 'U' : 'u') + 'serSelect', - prop = (prefix ? ('-' + prefix.toLowerCase() + '-') : '') + 'user-select'; - - e.style.cssText = prop + ': text;'; - if (e.style[propCC] == 'text') { - t = true; - return false; - } - - return true; - }); - - return t; -})(); -*/ - -if (!$.ui || !$.ui.widget) { - // duck punch $.cleanData like jQueryUI does to get that remove event - // https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.widget.js#L16-24 - var _cleanData = $.cleanData; - $.cleanData = function( elems ) { - for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { - try { - $( elem ).triggerHandler( "remove" ); - // http://bugs.jquery.com/ticket/8235 - } catch( e ) {} - } - _cleanData( elems ); - }; -} - -var // currently active contextMenu trigger - $currentTrigger = null, - // is contextMenu initialized with at least one menu? - initialized = false, - // window handle - $win = $(window), - // number of registered menus - counter = 0, - // mapping selector to namespace - namespaces = {}, - // mapping namespace to options - menus = {}, - // custom command type handlers - types = {}, - // default values - defaults = { - // selector of contextMenu trigger - selector: null, - // where to append the menu to - appendTo: null, - // method to trigger context menu ["right", "left", "hover"] - trigger: "right", - // hide menu when mouse leaves trigger / menu elements - autoHide: false, - // ms to wait before showing a hover-triggered context menu - delay: 200, - // flag denoting if a second trigger should simply move (true) or rebuild (false) an open menu - // as long as the trigger happened on one of the trigger-element's child nodes - reposition: true, - // determine position to show menu at - determinePosition: function($menu) { - // position to the lower middle of the trigger element - if ($.ui && $.ui.position) { - // .position() is provided as a jQuery UI utility - // (...and it won't work on hidden elements) - $menu.css('display', 'block').position({ - my: "center top", - at: "center bottom", - of: this, - offset: "0 5", - collision: "fit" - }).css('display', 'none'); - } else { - // determine contextMenu position - var offset = this.offset(); - offset.top += this.outerHeight(); - offset.left += this.outerWidth() / 2 - $menu.outerWidth() / 2; - $menu.css(offset); - } - }, - // position menu - position: function(opt, x, y) { - var $this = this, - offset; - // determine contextMenu position - if (!x && !y) { - opt.determinePosition.call(this, opt.$menu); - return; - } else if (x === "maintain" && y === "maintain") { - // x and y must not be changed (after re-show on command click) - offset = opt.$menu.position(); - } else { - // x and y are given (by mouse event) - offset = {top: y, left: x}; - } - - // correct offset if viewport demands it - var bottom = $win.scrollTop() + $win.height(), - right = $win.scrollLeft() + $win.width(), - height = opt.$menu.height(), - width = opt.$menu.width(); - - if (offset.top + height > bottom) { - offset.top -= height; - } - - if (offset.left + width > right) { - offset.left -= width; - } - - opt.$menu.css(offset); - }, - // position the sub-menu - positionSubmenu: function($menu) { - if ($.ui && $.ui.position) { - // .position() is provided as a jQuery UI utility - // (...and it won't work on hidden elements) - $menu.css('display', 'block').position({ - my: "left top", - at: "right top", - of: this, - collision: "flipfit fit" - }).css('display', ''); - } else { - // determine contextMenu position - var offset = { - top: 0, - left: this.outerWidth() - }; - $menu.css(offset); - } - }, - // offset to add to zIndex - zIndex: 1, - // show hide animation settings - animation: { - duration: 50, - show: 'slideDown', - hide: 'slideUp' - }, - // events - events: { - show: $.noop, - hide: $.noop - }, - // default callback - callback: null, - // list of contextMenu items - items: {} - }, - // mouse position for hover activation - hoveract = { - timer: null, - pageX: null, - pageY: null - }, - // determine zIndex - zindex = function($t) { - var zin = 0, - $tt = $t; - - while (true) { - zin = Math.max(zin, parseInt($tt.css('z-index'), 10) || 0); - $tt = $tt.parent(); - if (!$tt || !$tt.length || "html body".indexOf($tt.prop('nodeName').toLowerCase()) > -1 ) { - break; - } - } - - return zin; - }, - // event handlers - handle = { - // abort anything - abortevent: function(e){ - e.preventDefault(); - e.stopImmediatePropagation(); - }, - - // contextmenu show dispatcher - contextmenu: function(e) { - var $this = $(this); - - // disable actual context-menu - e.preventDefault(); - e.stopImmediatePropagation(); - - // abort native-triggered events unless we're triggering on right click - if (e.data.trigger != 'right' && e.originalEvent) { - return; - } - - // abort event if menu is visible for this trigger - if ($this.hasClass('context-menu-active')) { - return; - } - - if (!$this.hasClass('context-menu-disabled')) { - // theoretically need to fire a show event at - // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#context-menus - // var evt = jQuery.Event("show", { data: data, pageX: e.pageX, pageY: e.pageY, relatedTarget: this }); - // e.data.$menu.trigger(evt); - - $currentTrigger = $this; - if (e.data.build) { - var built = e.data.build($currentTrigger, e); - // abort if build() returned false - if (built === false) { - return; - } - - // dynamically build menu on invocation - e.data = $.extend(true, {}, defaults, e.data, built || {}); - - // abort if there are no items to display - if (!e.data.items || $.isEmptyObject(e.data.items)) { - // Note: jQuery captures and ignores errors from event handlers - if (window.console) { - (console.error || console.log)("No items specified to show in contextMenu"); - } - - throw new Error('No Items sepcified'); - } - - // backreference for custom command type creation - e.data.$trigger = $currentTrigger; - - op.create(e.data); - } - // show menu - op.show.call($this, e.data, e.pageX, e.pageY); - } - }, - // contextMenu left-click trigger - click: function(e) { - e.preventDefault(); - e.stopImmediatePropagation(); - $(this).trigger($.Event("contextmenu", { data: e.data, pageX: e.pageX, pageY: e.pageY })); - }, - // contextMenu right-click trigger - mousedown: function(e) { - // register mouse down - var $this = $(this); - - // hide any previous menus - if ($currentTrigger && $currentTrigger.length && !$currentTrigger.is($this)) { - $currentTrigger.data('contextMenu').$menu.trigger('contextmenu:hide'); - } - - // activate on right click - if (e.button == 2) { - $currentTrigger = $this.data('contextMenuActive', true); - } - }, - // contextMenu right-click trigger - mouseup: function(e) { - // show menu - var $this = $(this); - if ($this.data('contextMenuActive') && $currentTrigger && $currentTrigger.length && $currentTrigger.is($this) && !$this.hasClass('context-menu-disabled')) { - e.preventDefault(); - e.stopImmediatePropagation(); - $currentTrigger = $this; - $this.trigger($.Event("contextmenu", { data: e.data, pageX: e.pageX, pageY: e.pageY })); - } - - $this.removeData('contextMenuActive'); - }, - // contextMenu hover trigger - mouseenter: function(e) { - var $this = $(this), - $related = $(e.relatedTarget), - $document = $(document); - - // abort if we're coming from a menu - if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) { - return; - } - - // abort if a menu is shown - if ($currentTrigger && $currentTrigger.length) { - return; - } - - hoveract.pageX = e.pageX; - hoveract.pageY = e.pageY; - hoveract.data = e.data; - $document.on('mousemove.contextMenuShow', handle.mousemove); - hoveract.timer = setTimeout(function() { - hoveract.timer = null; - $document.off('mousemove.contextMenuShow'); - $currentTrigger = $this; - $this.trigger($.Event("contextmenu", { data: hoveract.data, pageX: hoveract.pageX, pageY: hoveract.pageY })); - }, e.data.delay ); - }, - // contextMenu hover trigger - mousemove: function(e) { - hoveract.pageX = e.pageX; - hoveract.pageY = e.pageY; - }, - // contextMenu hover trigger - mouseleave: function(e) { - // abort if we're leaving for a menu - var $related = $(e.relatedTarget); - if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) { - return; - } - - try { - clearTimeout(hoveract.timer); - } catch(e) {} - - hoveract.timer = null; - }, - - // click on layer to hide contextMenu - layerClick: function(e) { - var $this = $(this), - root = $this.data('contextMenuRoot'), - mouseup = false, - button = e.button, - x = e.pageX, - y = e.pageY, - target, - offset, - selectors; - - e.preventDefault(); - e.stopImmediatePropagation(); - - setTimeout(function() { - var $window, hideshow, possibleTarget; - var triggerAction = ((root.trigger == 'left' && button === 0) || (root.trigger == 'right' && button === 2)); - - // find the element that would've been clicked, wasn't the layer in the way - if (document.elementFromPoint) { - root.$layer.hide(); - target = document.elementFromPoint(x - $win.scrollLeft(), y - $win.scrollTop()); - root.$layer.show(); - } - - if (root.reposition && triggerAction) { - if (document.elementFromPoint) { - if (root.$trigger.is(target) || root.$trigger.has(target).length) { - root.position.call(root.$trigger, root, x, y); - return; - } - } else { - offset = root.$trigger.offset(); - $window = $(window); - // while this looks kinda awful, it's the best way to avoid - // unnecessarily calculating any positions - offset.top += $window.scrollTop(); - if (offset.top <= e.pageY) { - offset.left += $window.scrollLeft(); - if (offset.left <= e.pageX) { - offset.bottom = offset.top + root.$trigger.outerHeight(); - if (offset.bottom >= e.pageY) { - offset.right = offset.left + root.$trigger.outerWidth(); - if (offset.right >= e.pageX) { - // reposition - root.position.call(root.$trigger, root, x, y); - return; - } - } - } - } - } - } - - if (target && triggerAction) { - root.$trigger.one('contextmenu:hidden', function() { - $(target).contextMenu({x: x, y: y}); - }); - } - - root.$menu.trigger('contextmenu:hide'); - }, 50); - }, - // key handled :hover - keyStop: function(e, opt) { - if (!opt.isInput) { - e.preventDefault(); - } - - e.stopPropagation(); - }, - key: function(e) { - var opt = $currentTrigger.data('contextMenu') || {}; - - switch (e.keyCode) { - case 9: - case 38: // up - handle.keyStop(e, opt); - // if keyCode is [38 (up)] or [9 (tab) with shift] - if (opt.isInput) { - if (e.keyCode == 9 && e.shiftKey) { - e.preventDefault(); - opt.$selected && opt.$selected.find('input, textarea, select').blur(); - opt.$menu.trigger('prevcommand'); - return; - } else if (e.keyCode == 38 && opt.$selected.find('input, textarea, select').prop('type') == 'checkbox') { - // checkboxes don't capture this key - e.preventDefault(); - return; - } - } else if (e.keyCode != 9 || e.shiftKey) { - opt.$menu.trigger('prevcommand'); - return; - } - // omitting break; - - // case 9: // tab - reached through omitted break; - case 40: // down - handle.keyStop(e, opt); - if (opt.isInput) { - if (e.keyCode == 9) { - e.preventDefault(); - opt.$selected && opt.$selected.find('input, textarea, select').blur(); - opt.$menu.trigger('nextcommand'); - return; - } else if (e.keyCode == 40 && opt.$selected.find('input, textarea, select').prop('type') == 'checkbox') { - // checkboxes don't capture this key - e.preventDefault(); - return; - } - } else { - opt.$menu.trigger('nextcommand'); - return; - } - break; - - case 37: // left - handle.keyStop(e, opt); - if (opt.isInput || !opt.$selected || !opt.$selected.length) { - break; - } - - if (!opt.$selected.parent().hasClass('context-menu-root')) { - var $parent = opt.$selected.parent().parent(); - opt.$selected.trigger('contextmenu:blur'); - opt.$selected = $parent; - return; - } - break; - - case 39: // right - handle.keyStop(e, opt); - if (opt.isInput || !opt.$selected || !opt.$selected.length) { - break; - } - - var itemdata = opt.$selected.data('contextMenu') || {}; - if (itemdata.$menu && opt.$selected.hasClass('context-menu-submenu')) { - opt.$selected = null; - itemdata.$selected = null; - itemdata.$menu.trigger('nextcommand'); - return; - } - break; - - case 35: // end - case 36: // home - if (opt.$selected && opt.$selected.find('input, textarea, select').length) { - return; - } else { - (opt.$selected && opt.$selected.parent() || opt.$menu) - .children(':not(.disabled, .not-selectable)')[e.keyCode == 36 ? 'first' : 'last']() - .trigger('contextmenu:focus'); - e.preventDefault(); - return; - } - break; - - case 13: // enter - handle.keyStop(e, opt); - if (opt.isInput) { - if (opt.$selected && !opt.$selected.is('textarea, select')) { - e.preventDefault(); - return; - } - break; - } - opt.$selected && opt.$selected.trigger('mouseup'); - return; - - case 32: // space - case 33: // page up - case 34: // page down - // prevent browser from scrolling down while menu is visible - handle.keyStop(e, opt); - return; - - case 27: // esc - handle.keyStop(e, opt); - opt.$menu.trigger('contextmenu:hide'); - return; - - default: // 0-9, a-z - var k = (String.fromCharCode(e.keyCode)).toUpperCase(); - if (opt.accesskeys[k]) { - // according to the specs accesskeys must be invoked immediately - opt.accesskeys[k].$node.trigger(opt.accesskeys[k].$menu - ? 'contextmenu:focus' - : 'mouseup' - ); - return; - } - break; - } - // pass event to selected item, - // stop propagation to avoid endless recursion - e.stopPropagation(); - opt.$selected && opt.$selected.trigger(e); - }, - - // select previous possible command in menu - prevItem: function(e) { - e.stopPropagation(); - var opt = $(this).data('contextMenu') || {}; - - // obtain currently selected menu - if (opt.$selected) { - var $s = opt.$selected; - opt = opt.$selected.parent().data('contextMenu') || {}; - opt.$selected = $s; - } - - var $children = opt.$menu.children(), - $prev = !opt.$selected || !opt.$selected.prev().length ? $children.last() : opt.$selected.prev(), - $round = $prev; - - // skip disabled - while ($prev.hasClass('disabled') || $prev.hasClass('not-selectable')) { - if ($prev.prev().length) { - $prev = $prev.prev(); - } else { - $prev = $children.last(); - } - if ($prev.is($round)) { - // break endless loop - return; - } - } - - // leave current - if (opt.$selected) { - handle.itemMouseleave.call(opt.$selected.get(0), e); - } - - // activate next - handle.itemMouseenter.call($prev.get(0), e); - - // focus input - var $input = $prev.find('input, textarea, select'); - if ($input.length) { - $input.focus(); - } - }, - // select next possible command in menu - nextItem: function(e) { - e.stopPropagation(); - var opt = $(this).data('contextMenu') || {}; - - // obtain currently selected menu - if (opt.$selected) { - var $s = opt.$selected; - opt = opt.$selected.parent().data('contextMenu') || {}; - opt.$selected = $s; - } - - var $children = opt.$menu.children(), - $next = !opt.$selected || !opt.$selected.next().length ? $children.first() : opt.$selected.next(), - $round = $next; - - // skip disabled - while ($next.hasClass('disabled') || $next.hasClass('not-selectable')) { - if ($next.next().length) { - $next = $next.next(); - } else { - $next = $children.first(); - } - if ($next.is($round)) { - // break endless loop - return; - } - } - - // leave current - if (opt.$selected) { - handle.itemMouseleave.call(opt.$selected.get(0), e); - } - - // activate next - handle.itemMouseenter.call($next.get(0), e); - - // focus input - var $input = $next.find('input, textarea, select'); - if ($input.length) { - $input.focus(); - } - }, - - // flag that we're inside an input so the key handler can act accordingly - focusInput: function(e) { - var $this = $(this).closest('.context-menu-item'), - data = $this.data(), - opt = data.contextMenu, - root = data.contextMenuRoot; - - root.$selected = opt.$selected = $this; - root.isInput = opt.isInput = true; - }, - // flag that we're inside an input so the key handler can act accordingly - blurInput: function(e) { - var $this = $(this).closest('.context-menu-item'), - data = $this.data(), - opt = data.contextMenu, - root = data.contextMenuRoot; - - root.isInput = opt.isInput = false; - }, - - // :hover on menu - menuMouseenter: function(e) { - var root = $(this).data().contextMenuRoot; - root.hovering = true; - }, - // :hover on menu - menuMouseleave: function(e) { - var root = $(this).data().contextMenuRoot; - if (root.$layer && root.$layer.is(e.relatedTarget)) { - root.hovering = false; - } - }, - - // :hover done manually so key handling is possible - itemMouseenter: function(e) { - var $this = $(this), - data = $this.data(), - opt = data.contextMenu, - root = data.contextMenuRoot; - - root.hovering = true; - - // abort if we're re-entering - if (e && root.$layer && root.$layer.is(e.relatedTarget)) { - e.preventDefault(); - e.stopImmediatePropagation(); - } - - // make sure only one item is selected - (opt.$menu ? opt : root).$menu - .children('.hover').trigger('contextmenu:blur'); - - if ($this.hasClass('disabled') || $this.hasClass('not-selectable')) { - opt.$selected = null; - return; - } - - $this.trigger('contextmenu:focus'); - }, - // :hover done manually so key handling is possible - itemMouseleave: function(e) { - var $this = $(this), - data = $this.data(), - opt = data.contextMenu, - root = data.contextMenuRoot; - - if (root !== opt && root.$layer && root.$layer.is(e.relatedTarget)) { - root.$selected && root.$selected.trigger('contextmenu:blur'); - e.preventDefault(); - e.stopImmediatePropagation(); - root.$selected = opt.$selected = opt.$node; - return; - } - - $this.trigger('contextmenu:blur'); - }, - // contextMenu item click - itemClick: function(e) { - var $this = $(this), - data = $this.data(), - opt = data.contextMenu, - root = data.contextMenuRoot, - key = data.contextMenuKey, - callback; - - // abort if the key is unknown or disabled or is a menu - if (!opt.items[key] || $this.is('.disabled, .context-menu-submenu, .context-menu-separator, .not-selectable')) { - return; - } - - e.preventDefault(); - e.stopImmediatePropagation(); - - if ($.isFunction(root.callbacks[key]) && Object.prototype.hasOwnProperty.call(root.callbacks, key)) { - // item-specific callback - callback = root.callbacks[key]; - } else if ($.isFunction(root.callback)) { - // default callback - callback = root.callback; - } else { - // no callback, no action - return; - } - - // hide menu if callback doesn't stop that - if (callback.call(root.$trigger, key, root) !== false) { - root.$menu.trigger('contextmenu:hide'); - } else if (root.$menu.parent().length) { - op.update.call(root.$trigger, root); - } - }, - // ignore click events on input elements - inputClick: function(e) { - e.stopImmediatePropagation(); - }, - - // hide - hideMenu: function(e, data) { - var root = $(this).data('contextMenuRoot'); - op.hide.call(root.$trigger, root, data && data.force); - }, - // focus - focusItem: function(e) { - e.stopPropagation(); - var $this = $(this), - data = $this.data(), - opt = data.contextMenu, - root = data.contextMenuRoot; - - $this.addClass('hover') - .siblings('.hover').trigger('contextmenu:blur'); - - // remember selected - opt.$selected = root.$selected = $this; - - // position sub-menu - do after show so dumb $.ui.position can keep up - if (opt.$node) { - root.positionSubmenu.call(opt.$node, opt.$menu); - } - }, - // blur - blurItem: function(e) { - e.stopPropagation(); - var $this = $(this), - data = $this.data(), - opt = data.contextMenu, - root = data.contextMenuRoot; - - $this.removeClass('hover'); - opt.$selected = null; - } - }, - // operations - op = { - show: function(opt, x, y) { - var $trigger = $(this), - offset, - css = {}; - - // hide any open menus - $('#context-menu-layer').trigger('mousedown'); - - // backreference for callbacks - opt.$trigger = $trigger; - - // show event - if (opt.events.show.call($trigger, opt) === false) { - $currentTrigger = null; - return; - } - - // create or update context menu - op.update.call($trigger, opt); - - // position menu - opt.position.call($trigger, opt, x, y); - - // make sure we're in front - if (opt.zIndex) { - css.zIndex = zindex($trigger) + opt.zIndex; - } - - // add layer - op.layer.call(opt.$menu, opt, css.zIndex); - - // adjust sub-menu zIndexes - opt.$menu.find('ul').css('zIndex', css.zIndex + 1); - - // position and show context menu - opt.$menu.css( css )[opt.animation.show](opt.animation.duration, function() { - $trigger.trigger('contextmenu:visible'); - }); - // make options available and set state - $trigger - .data('contextMenu', opt) - .addClass("context-menu-active"); - - // register key handler - $(document).off('keydown.contextMenu').on('keydown.contextMenu', handle.key); - // register autoHide handler - if (opt.autoHide) { - // mouse position handler - $(document).on('mousemove.contextMenuAutoHide', function(e) { - // need to capture the offset on mousemove, - // since the page might've been scrolled since activation - var pos = $trigger.offset(); - pos.right = pos.left + $trigger.outerWidth(); - pos.bottom = pos.top + $trigger.outerHeight(); - - if (opt.$layer && !opt.hovering && (!(e.pageX >= pos.left && e.pageX <= pos.right) || !(e.pageY >= pos.top && e.pageY <= pos.bottom))) { - // if mouse in menu... - opt.$menu.trigger('contextmenu:hide'); - } - }); - } - }, - hide: function(opt, force) { - var $trigger = $(this); - if (!opt) { - opt = $trigger.data('contextMenu') || {}; - } - - // hide event - if (!force && opt.events && opt.events.hide.call($trigger, opt) === false) { - return; - } - - // remove options and revert state - $trigger - .removeData('contextMenu') - .removeClass("context-menu-active"); - - if (opt.$layer) { - // keep layer for a bit so the contextmenu event can be aborted properly by opera - setTimeout((function($layer) { - return function(){ - $layer.remove(); - }; - })(opt.$layer), 10); - - try { - delete opt.$layer; - } catch(e) { - opt.$layer = null; - } - } - - // remove handle - $currentTrigger = null; - // remove selected - opt.$menu.find('.hover').trigger('contextmenu:blur'); - opt.$selected = null; - // unregister key and mouse handlers - //$(document).off('.contextMenuAutoHide keydown.contextMenu'); // http://bugs.jquery.com/ticket/10705 - $(document).off('.contextMenuAutoHide').off('keydown.contextMenu'); - // hide menu - opt.$menu && opt.$menu[opt.animation.hide](opt.animation.duration, function (){ - // tear down dynamically built menu after animation is completed. - if (opt.build) { - opt.$menu.remove(); - $.each(opt, function(key, value) { - switch (key) { - case 'ns': - case 'selector': - case 'build': - case 'trigger': - return true; - - default: - opt[key] = undefined; - try { - delete opt[key]; - } catch (e) {} - return true; - } - }); - } - - setTimeout(function() { - $trigger.trigger('contextmenu:hidden'); - }, 10); - }); - }, - create: function(opt, root) { - if (root === undefined) { - root = opt; - } - // create contextMenu - opt.$menu = $('
      ').addClass(opt.className || "").data({ - 'contextMenu': opt, - 'contextMenuRoot': root - }); - - $.each(['callbacks', 'commands', 'inputs'], function(i,k){ - opt[k] = {}; - if (!root[k]) { - root[k] = {}; - } - }); - - root.accesskeys || (root.accesskeys = {}); - - // create contextMenu items - $.each(opt.items, function(key, item){ - var $t = $('
    • ').addClass(item.className || ""), - $label = null, - $input = null; - - // iOS needs to see a click-event bound to an element to actually - // have the TouchEvents infrastructure trigger the click event - $t.on('click', $.noop); - - item.$node = $t.data({ - 'contextMenu': opt, - 'contextMenuRoot': root, - 'contextMenuKey': key - }); - - // register accesskey - // NOTE: the accesskey attribute should be applicable to any element, but Safari5 and Chrome13 still can't do that - if (item.accesskey) { - var aks = splitAccesskey(item.accesskey); - for (var i=0, ak; ak = aks[i]; i++) { - if (!root.accesskeys[ak]) { - root.accesskeys[ak] = item; - item._name = item.name.replace(new RegExp('(' + ak + ')', 'i'), '$1'); - break; - } - } - } - - if (typeof item == "string") { - $t.addClass('context-menu-separator not-selectable'); - } else if (item.type && types[item.type]) { - // run custom type handler - types[item.type].call($t, item, opt, root); - // register commands - $.each([opt, root], function(i,k){ - k.commands[key] = item; - if ($.isFunction(item.callback)) { - k.callbacks[key] = item.callback; - } - }); - } else { - // add label for input - if (item.type == 'html') { - $t.addClass('context-menu-html not-selectable'); - } else if (item.type) { - $label = $('').appendTo($t); - $('').html(item._name || item.name).appendTo($label); - $t.addClass('context-menu-input'); - opt.hasTypes = true; - $.each([opt, root], function(i,k){ - k.commands[key] = item; - k.inputs[key] = item; - }); - } else if (item.items) { - item.type = 'sub'; - } - - switch (item.type) { - case 'text': - $input = $('') - .attr('name', 'context-menu-input-' + key) - .val(item.value || "") - .appendTo($label); - break; - - case 'textarea': - $input = $('') - .attr('name', 'context-menu-input-' + key) - .val(item.value || "") - .appendTo($label); - - if (item.height) { - $input.height(item.height); - } - break; - - case 'checkbox': - $input = $('') - .attr('name', 'context-menu-input-' + key) - .val(item.value || "") - .prop("checked", !!item.selected) - .prependTo($label); - break; - - case 'radio': - $input = $('') - .attr('name', 'context-menu-input-' + item.radio) - .val(item.value || "") - .prop("checked", !!item.selected) - .prependTo($label); - break; - - case 'select': - $input = $(' - if (item.type && item.type != 'sub' && item.type != 'html') { - $input - .on('focus', handle.focusInput) - .on('blur', handle.blurInput); - - if (item.events) { - $input.on(item.events, opt); - } - } - - // add icons - if (item.icon) { - $t.addClass("icon icon-" + item.icon); - } - } - - // cache contained elements - item.$input = $input; - item.$label = $label; - - // attach item to menu - $t.appendTo(opt.$menu); - - // Disable text selection - if (!opt.hasTypes && $.support.eventSelectstart) { - // browsers support user-select: none, - // IE has a special event for text-selection - // browsers supporting neither will not be preventing text-selection - $t.on('selectstart.disableTextSelect', handle.abortevent); - } - }); - // attach contextMenu to (to bypass any possible overflow:hidden issues on parents of the trigger element) - if (!opt.$node) { - opt.$menu.css('display', 'none').addClass('context-menu-root'); - } - opt.$menu.appendTo(opt.appendTo || document.body); - }, - resize: function($menu, nested) { - // determine widths of submenus, as CSS won't grow them automatically - // position:absolute within position:absolute; min-width:100; max-width:200; results in width: 100; - // kinda sucks hard... - - // determine width of absolutely positioned element - $menu.css({position: 'absolute', display: 'block'}); - // don't apply yet, because that would break nested elements' widths - // add a pixel to circumvent word-break issue in IE9 - #80 - $menu.data('width', Math.ceil($menu.width()) + 1); - // reset styles so they allow nested elements to grow/shrink naturally - $menu.css({ - position: 'static', - minWidth: '0px', - maxWidth: '100000px' - }); - // identify width of nested menus - $menu.find('> li > ul').each(function() { - op.resize($(this), true); - }); - // reset and apply changes in the end because nested - // elements' widths wouldn't be calculatable otherwise - if (!nested) { - $menu.find('ul').andSelf().css({ - position: '', - display: '', - minWidth: '', - maxWidth: '' - }).width(function() { - return $(this).data('width'); - }); - } - }, - update: function(opt, root) { - var $trigger = this; - if (root === undefined) { - root = opt; - op.resize(opt.$menu); - } - // re-check disabled for each item - opt.$menu.children().each(function(){ - var $item = $(this), - key = $item.data('contextMenuKey'), - item = opt.items[key], - disabled = ($.isFunction(item.disabled) && item.disabled.call($trigger, key, root)) || item.disabled === true; - - // dis- / enable item - $item[disabled ? 'addClass' : 'removeClass']('disabled'); - - if (item.type) { - // dis- / enable input elements - $item.find('input, select, textarea').prop('disabled', disabled); - - // update input states - switch (item.type) { - case 'text': - case 'textarea': - item.$input.val(item.value || ""); - break; - - case 'checkbox': - case 'radio': - item.$input.val(item.value || "").prop('checked', !!item.selected); - break; - - case 'select': - item.$input.val(item.selected || ""); - break; - } - } - - if (item.$menu) { - // update sub-menu - op.update.call($trigger, item, root); - } - }); - }, - layer: function(opt, zIndex) { - // add transparent layer for click area - // filter and background for Internet Explorer, Issue #23 - var $layer = opt.$layer = $('
      ') - .css({height: $win.height(), width: $win.width(), display: 'block'}) - .data('contextMenuRoot', opt) - .insertBefore(this) - .on('contextmenu', handle.abortevent) - .on('mousedown', handle.layerClick); - - // IE6 doesn't know position:fixed; - if (!$.support.fixedPosition) { - $layer.css({ - 'position' : 'absolute', - 'height' : $(document).height() - }); - } - - return $layer; - } - }; - -// split accesskey according to http://www.whatwg.org/specs/web-apps/current-work/multipage/editing.html#assigned-access-key -function splitAccesskey(val) { - var t = val.split(/\s+/), - keys = []; - - for (var i=0, k; k = t[i]; i++) { - k = k[0].toUpperCase(); // first character only - // theoretically non-accessible characters should be ignored, but different systems, different keyboard layouts, ... screw it. - // a map to look up already used access keys would be nice - keys.push(k); - } - - return keys; -} - -// handle contextMenu triggers -$.fn.contextMenu = function(operation) { - if (operation === undefined) { - this.first().trigger('contextmenu'); - } else if (operation.x && operation.y) { - this.first().trigger($.Event("contextmenu", {pageX: operation.x, pageY: operation.y})); - } else if (operation === "hide") { - var $menu = this.data('contextMenu').$menu; - $menu && $menu.trigger('contextmenu:hide'); - } else if (operation === "destroy") { - $.contextMenu("destroy", {context: this}); - } else if ($.isPlainObject(operation)) { - operation.context = this; - $.contextMenu("create", operation); - } else if (operation) { - this.removeClass('context-menu-disabled'); - } else if (!operation) { - this.addClass('context-menu-disabled'); - } - - return this; -}; - -// manage contextMenu instances -$.contextMenu = function(operation, options) { - if (typeof operation != 'string') { - options = operation; - operation = 'create'; - } - - if (typeof options == 'string') { - options = {selector: options}; - } else if (options === undefined) { - options = {}; - } - - // merge with default options - var o = $.extend(true, {}, defaults, options || {}); - var $document = $(document); - var $context = $document; - var _hasContext = false; - - if (!o.context || !o.context.length) { - o.context = document; - } else { - // you never know what they throw at you... - $context = $(o.context).first(); - o.context = $context.get(0); - _hasContext = o.context !== document; - } - - switch (operation) { - case 'create': - // no selector no joy - if (!o.selector) { - throw new Error('No selector specified'); - } - // make sure internal classes are not bound to - if (o.selector.match(/.context-menu-(list|item|input)($|\s)/)) { - throw new Error('Cannot bind to selector "' + o.selector + '" as it contains a reserved className'); - } - if (!o.build && (!o.items || $.isEmptyObject(o.items))) { - throw new Error('No Items sepcified'); - } - counter ++; - o.ns = '.contextMenu' + counter; - if (!_hasContext) { - namespaces[o.selector] = o.ns; - } - menus[o.ns] = o; - - // default to right click - if (!o.trigger) { - o.trigger = 'right'; - } - - if (!initialized) { - // make sure item click is registered first - $document - .on({ - 'contextmenu:hide.contextMenu': handle.hideMenu, - 'prevcommand.contextMenu': handle.prevItem, - 'nextcommand.contextMenu': handle.nextItem, - 'contextmenu.contextMenu': handle.abortevent, - 'mouseenter.contextMenu': handle.menuMouseenter, - 'mouseleave.contextMenu': handle.menuMouseleave - }, '.context-menu-list') - .on('mouseup.contextMenu', '.context-menu-input', handle.inputClick) - .on({ - 'mouseup.contextMenu': handle.itemClick, - 'contextmenu:focus.contextMenu': handle.focusItem, - 'contextmenu:blur.contextMenu': handle.blurItem, - 'contextmenu.contextMenu': handle.abortevent, - 'mouseenter.contextMenu': handle.itemMouseenter, - 'mouseleave.contextMenu': handle.itemMouseleave - }, '.context-menu-item'); - - initialized = true; - } - - // engage native contextmenu event - $context - .on('contextmenu' + o.ns, o.selector, o, handle.contextmenu); - - if (_hasContext) { - // add remove hook, just in case - $context.on('remove' + o.ns, function() { - $(this).contextMenu("destroy"); - }); - } - - switch (o.trigger) { - case 'hover': - $context - .on('mouseenter' + o.ns, o.selector, o, handle.mouseenter) - .on('mouseleave' + o.ns, o.selector, o, handle.mouseleave); - break; - - case 'left': - $context.on('click' + o.ns, o.selector, o, handle.click); - break; - /* - default: - // http://www.quirksmode.org/dom/events/contextmenu.html - $document - .on('mousedown' + o.ns, o.selector, o, handle.mousedown) - .on('mouseup' + o.ns, o.selector, o, handle.mouseup); - break; - */ - } - - // create menu - if (!o.build) { - op.create(o); - } - break; - - case 'destroy': - var $visibleMenu; - if (_hasContext) { - // get proper options - var context = o.context; - $.each(menus, function(ns, o) { - if (o.context !== context) { - return true; - } - - $visibleMenu = $('.context-menu-list').filter(':visible'); - if ($visibleMenu.length && $visibleMenu.data().contextMenuRoot.$trigger.is($(o.context).find(o.selector))) { - $visibleMenu.trigger('contextmenu:hide', {force: true}); - } - - try { - if (menus[o.ns].$menu) { - menus[o.ns].$menu.remove(); - } - - delete menus[o.ns]; - } catch(e) { - menus[o.ns] = null; - } - - $(o.context).off(o.ns); - - return true; - }); - } else if (!o.selector) { - $document.off('.contextMenu .contextMenuAutoHide'); - $.each(menus, function(ns, o) { - $(o.context).off(o.ns); - }); - - namespaces = {}; - menus = {}; - counter = 0; - initialized = false; - - $('#context-menu-layer, .context-menu-list').remove(); - } else if (namespaces[o.selector]) { - $visibleMenu = $('.context-menu-list').filter(':visible'); - if ($visibleMenu.length && $visibleMenu.data().contextMenuRoot.$trigger.is(o.selector)) { - $visibleMenu.trigger('contextmenu:hide', {force: true}); - } - - try { - if (menus[namespaces[o.selector]].$menu) { - menus[namespaces[o.selector]].$menu.remove(); - } - - delete menus[namespaces[o.selector]]; - } catch(e) { - menus[namespaces[o.selector]] = null; - } - - $document.off(namespaces[o.selector]); - } - break; - - case 'html5': - // if or are not handled by the browser, - // or options was a bool true, - // initialize $.contextMenu for them - if ((!$.support.htmlCommand && !$.support.htmlMenuitem) || (typeof options == "boolean" && options)) { - $('menu[type="context"]').each(function() { - if (this.id) { - $.contextMenu({ - selector: '[contextmenu=' + this.id +']', - items: $.contextMenu.fromMenu(this) - }); - } - }).css('display', 'none'); - } - break; - - default: - throw new Error('Unknown operation "' + operation + '"'); - } - - return this; -}; - -// import values into commands -$.contextMenu.setInputValues = function(opt, data) { - if (data === undefined) { - data = {}; - } - - $.each(opt.inputs, function(key, item) { - switch (item.type) { - case 'text': - case 'textarea': - item.value = data[key] || ""; - break; - - case 'checkbox': - item.selected = data[key] ? true : false; - break; - - case 'radio': - item.selected = (data[item.radio] || "") == item.value ? true : false; - break; - - case 'select': - item.selected = data[key] || ""; - break; - } - }); -}; - -// export values from commands -$.contextMenu.getInputValues = function(opt, data) { - if (data === undefined) { - data = {}; - } - - $.each(opt.inputs, function(key, item) { - switch (item.type) { - case 'text': - case 'textarea': - case 'select': - data[key] = item.$input.val(); - break; - - case 'checkbox': - data[key] = item.$input.prop('checked'); - break; - - case 'radio': - if (item.$input.prop('checked')) { - data[item.radio] = item.value; - } - break; - } - }); - - return data; -}; - -// find